Commit Graph

11114 Commits

Author SHA1 Message Date
Claude 0ca3ff71eb perf: fuse 128-bit multiply for K/Native (saves 16 mul per field op)
Add fieldMulReduceFused and fieldSqrReduceFused that compute both lo
and hi of each 64×64→128 multiply from 4 shared sub-products.

Previous: lo = a * b (1 hardware mul) + hi = umulh(a,b) (4 imul) = 5
Fused: 4 shared sub-products compute both lo and hi = 4 multiplies

The inline lambda consumer pattern avoids allocation:
  mulFull(a, b) { lo, hi -> ... }  // everything inlined at call site

K/Native benchmark improvement (linuxX64):
  verifySchnorrFast: 101,637 → 96,699 ns (-5%)
  signSchnorr(cached): 50,206 → 45,037 ns (-10%)
  compressedPubKeyFor: 40,906 → 37,075 ns (-9%)
  Native instruction count: 95 → 79 multiplies per field mul

Android keeps the unfused path because ART's single-cycle MUL for
a*b is faster than manually constructing lo from sub-products.
The fused approach saves 1 multiply but adds ~5 shift/and/or ops —
net regression on ART (80,756 vs 74,898 ns for verify).

https://claude.ai/code/session_015CtM5k88rF7WFgX8o2AGNR
2026-04-09 20:45:50 +00:00
Claude 0c51dbcf74 docs: document failed UMULH approaches on Android
Remove crypto-intrinsics module and API-tiered dispatch files.
Revert to the pure-Kotlin fallback for Android field multiply.

Three approaches to reach hardware UMULH were tested on Pixel 8
(Android 16, API 36) and all failed:

1. Direct Math.unsignedMultiplyHigh from Kotlin:
   D8 replaces with pure-Java backport when app minSdk=26 < 35.

2. MethodHandle.invokeExact via Java helper module:
   Bytecode correct (invoke-polymorphic JJ→J, zero boxing), but
   ART can't inline invoke-polymorphic. 25ns/call → 2.2x regression.

3. Full fieldMulReduce in Java, module with minSdk=35:
   D8 runs at APP level with app's minSdk=26, not library's minSdk=35.
   Trace confirmed: FieldMulIntrinsic$$ExternalSyntheticBackport0.m
   still generated. Plus Long.compareUnsigned (282ns/call) is slower
   than Kotlin's inlined XOR trick (~0ns).

The Kotlin inline+crossinline pattern remains optimal for ART:
- unsignedMultiplyHighFallback inlined at each call site
- uLtInline uses XOR+compare (no method call)
- Fused function fits ART's inlining budget

UMULH on Android requires raising app minSdk to 35.

https://claude.ai/code/session_015CtM5k88rF7WFgX8o2AGNR
2026-04-09 19:59:40 +00:00
Claude 7f8dba4af8 perf: add API-tiered field multiply dispatch for Android
Add 3-file dispatch structure for fieldMulReduce on Android:
- FieldMulApi35: placeholder for Math.unsignedMultiplyHigh (API 35+)
- FieldMulApi31: placeholder for Math.multiplyHigh (API 31-34)
- Fallback: pure-Kotlin 4-imul (API <31, unchanged)

All three currently use the pure-Kotlin fallback because both paths
to the hardware UMULH intrinsic are blocked:

1. Direct Math.unsignedMultiplyHigh call: D8 replaces with synthetic
   backport (ExternalSyntheticBackport0.m) when minSdk=26 < 35.
   Verified via dexdump: backport is pure-Java 4-imul, never reaches
   hardware UMULH even on API 36 devices.

2. MethodHandle.invokeExact: Kotlin compiles as regular invokevirtual
   with Object[] boxing (3 allocs per call), not type-exact
   invoke-polymorphic (JJ)J. Only Java's javac has @PolymorphicSignature
   support. KMP androidMain doesn't support Java sources.

TO UNLOCK: Add a Java helper class (MulHighInvoker.java) in a separate
Android library module that calls MethodHandle.invokeExact(long, long)
with zero boxing. This produces invoke-polymorphic that D8 cannot
desugar and ART intrinsifies to UMULH on ARM64.

https://claude.ai/code/session_015CtM5k88rF7WFgX8o2AGNR
2026-04-09 19:10:37 +00:00
Claude 34bef2438e perf: remove lazy delegates from ECPoint precomputed tables
Convert gOddTable, gLamTable, and combTable from `by lazy` to eager
initialization. These tables are accessed on every verify (mulDoubleG)
and sign (mulG) call.

On K/Native, each `by lazy` access goes through
SynchronizedLazyImpl.getValue() — a thread-safe lock check + memory
barrier. The native disassembly showed 2 SynchronizedLazyImpl calls
per mulDoubleG invocation.

On JVM, Kotlin's lazy delegates use double-checked locking via
SynchronizedLazyImpl, adding interface dispatch + volatile read on
every access.

Move the `scratch` (ScratchLocal) field declaration before the table
fields to satisfy initialization order — buildGOddTable() and
buildCombTable() use doublePoint/addPoints which call scratch.get().

https://claude.ai/code/session_015CtM5k88rF7WFgX8o2AGNR
2026-04-09 18:01:15 +00:00
Claude 39ce3ad260 perf: optimize secp256k1 verify bytecode — fix JIT/branch issues
Three bytecode-level optimizations to the Schnorr verify path:

1. Replace `by lazy` delegates with direct field init for tag hash
   prefixes (CHALLENGE_PREFIX, AUX_PREFIX, NONCE_PREFIX). Eliminates
   Lazy.getValue() interface dispatch + checkcast on every call.

2. Use explicit copyInto parameters everywhere, eliminating the
   copyInto$default bridge method (bitmask + 3 branches + arraylength
   per call). 4 calls per verify × 3 using defaults = 12 extra branches
   removed from the hot path.

3. Extract shared verify computation into verifySchnorrCore(), called
   by both verifySchnorr() and verifySchnorrFast(). Previously, two
   ~400-bytecode near-identical methods competed for JIT optimization.
   Now one hot method gets compiled, and both public methods are thin
   wrappers (34 and 130 bytecodes).

JVM benchmark before: verifySchnorrFast was 5% SLOWER than verifySchnorr
(60,930 vs 57,876 ns) due to JIT warmup ordering bias.

JVM benchmark after: verifySchnorrFast is 11% faster (50,345 vs 56,143 ns),
matching the expected ~14% savings from skipping the field inversion.

https://claude.ai/code/session_015CtM5k88rF7WFgX8o2AGNR
2026-04-09 16:43:20 +00:00
Vitor Pamplona 8710c30261 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2026-04-09 11:36:05 -04:00
Vitor Pamplona 1361353995 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2026-04-09 11:25:42 -04:00
Vitor Pamplona 45436aa4b2 Merge pull request #2190 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-04-09 11:22:07 -04:00
Crowdin Bot b9fbd77309 New Crowdin translations by GitHub Action 2026-04-09 15:14:37 +00:00
Vitor Pamplona eb32ea7fdc Merge pull request #2188 from vitorpamplona/claude/optimize-secp256k1-TFUD5
Optimize secp256k1 field arithmetic with fused multiply-reduce and platform-specific intrinsics
2026-04-09 11:12:47 -04:00
Claude da64a4a4d3 fix: align all 3 benchmarks to identical operation set
All 3 platform benchmarks (Android, JVM, K/Native) now measure the
same core operations for cross-platform comparability:

  verifySchnorr        — strict BIP-340
  verifySchnorrFast    — Nostr x-check only (no y-parity inversion)
  signSchnorr          — derives pubkey each time
  signSchnorr(cached)  — pre-computed x-only pubkey
  compressedPubKeyFor  — create + compress
  secKeyVerify         — key validation
  privKeyTweakAdd      — BIP-32 key derivation
  ecdhXOnly            — Nostr ECDH (NIP-04/44)
  batch verify (8, 16) — same-pubkey batch

Changes:
- Android: added signSchnorrCachedPkOurs, ecdhXOnly/ecdhXOnlyOurs
  (replaced pubKeyTweakMulCompact naming)
- JVM: removed pubkeyCreate and pubKeyCompress (subsets of
  compressedPubKeyFor, not measured on other platforms)
- K/Native: added verifySchnorrFast

K/Native retains field micro-benchmarks (FieldP.mul/sqr/add/sub/inv,
unsignedMultiplyHigh, uLt) as a K/N-specific diagnostic tool.

https://claude.ai/code/session_01EMY5RnXb9rnsyU2KbXrSaY
2026-04-09 15:02:39 +00:00
Claude b3206b8e48 feat: add batch verify to Android benchmark + expose in Secp256k1InstanceOurs
Add verifySchnorrBatch to Secp256k1InstanceOurs wrapper and add
Android benchmark tests for batch(8) and batch(16).

To get per-event cost: ns/op ÷ batchSize.
JVM benchmark showed 4-7× speedup over individual verify.
Android results TBD.

https://claude.ai/code/session_01EMY5RnXb9rnsyU2KbXrSaY
2026-04-09 14:45:14 +00:00
Claude 245212727a perf: keep uLt (expect/actual) for non-fused code, uLtInline only in fused
The previous commit replaced ALL uLt calls with uLtInline (XOR trick),
which regressed JVM verify from 1.6× to 1.9× vs native C. The XOR trick
is slower than Long.compareUnsigned on HotSpot.

Fix: uLtInline is used ONLY inside the fused FieldMulPlatform.kt inline
functions (which JVM doesn't use — it uses the unfused path). All other
code (U256.addTo/subTo, FieldP.add/sub/half, ScalarN, Glv) keeps the
expect/actual uLt which uses Long.compareUnsigned on JVM.

JVM: verifySchnorrFast 1.3× (restored, improved)
Android: uLt overhead remains for non-fused paths (~11K calls/verify)
but the fused mul/sqr path (the dominant cost) is fully inlined.

https://claude.ai/code/session_01EMY5RnXb9rnsyU2KbXrSaY
2026-04-09 13:55:59 +00:00
Claude e408db0944 perf: inline unsignedMultiplyHighFallback to eliminate 32K function calls
Trace profiling showed unsignedMultiplyHighFallback at 16.9% of verify
time (32,524 calls × 82ns = 2.674ms). Although called from inside an
inline crossinline lambda, the function itself was a regular dispatch.

Adding @Suppress("NOTHING_TO_INLINE") inline makes the Kotlin compiler
embed the 4-multiply arithmetic directly at each call site, eliminating
all function dispatch overhead.

https://claude.ai/code/session_01EMY5RnXb9rnsyU2KbXrSaY
2026-04-09 13:27:25 +00:00
Claude c243e3894c perf: eliminate D8 backport + uLt overhead on Android (38% of verify)
Trace profiling on Pixel 8 revealed two massive overhead sources:

1. ExternalSyntheticBackport0.m — 17.5% of verify (3.45ms)
   D8/R8 desugaring wraps Math.unsignedMultiplyHigh in a synthetic
   backport because minSdk=26 < 35. The backport adds 139ns per call
   × 24,875 calls. The actual UMULH intrinsic is ~1ns, but the
   wrapper adds ~138ns.

   FIX: Use pure-Kotlin unsignedMultiplyHighFallback on Android instead
   of Math.unsignedMultiplyHigh. The fallback (4 Long multiplies +
   shifts, ~10-20ns) is FASTER than the backported intrinsic (139ns).
   Removed all API-level dispatch — a single fallback path for all
   Android versions.

2. uLt function calls — 20.7% of verify (4.08ms)
   The expect/actual uLt (can't be inline) was called 49,924 times
   per verify at 82ns each. Most calls came from the fused
   fieldMulReduceWith/fieldSqrReduceWith inline expansions.

   FIX: Add uLtInline — a private inline function using the XOR trick
   directly. Since it's @Suppress("NOTHING_TO_INLINE") inline, the
   Kotlin compiler inlines it at every call site (not the JIT).
   Replaces all 55 uLt calls in the fused inline functions.
   JVM is unaffected (uses unfused path with Long.compareUnsigned).

Combined: eliminates ~38% of verify overhead on Android.

https://claude.ai/code/session_01EMY5RnXb9rnsyU2KbXrSaY
2026-04-09 13:13:40 +00:00
Vitor Pamplona 4610eda8e2 Merge pull request #2189 from davotoula/claude/gif-to-mp4-conversion-g1cve
Animated gif to mp4 conversion
2026-04-09 08:54:39 -04:00
Claude fbcd0d6326 feat: expose verifySchnorrFast in Secp256k1InstanceOurs + Android benchmark
Wire verifySchnorrFast (x-check only, no y-parity inversion) into:
- Secp256k1InstanceOurs wrapper for app-level usage
- Android benchmark as verifySchnorrFastOurs for Pixel 8 measurement

Expected: ~15% faster than verifySchnorrOurs (~100μs vs ~120μs on Pixel 8)

https://claude.ai/code/session_01EMY5RnXb9rnsyU2KbXrSaY
2026-04-09 12:33:13 +00:00
davotoula 58c30901e8 perf: run GIF-to-MP4 conversion on Dispatchers.Default
The conversion pipeline is overwhelmingly CPU/GPU bound (Movie decode,
GL rendering, MediaCodec encode) and can run for several seconds on a
large GIF. Running it on Dispatchers.IO occupies a thread from the
large IO pool with no kernel wait, which can starve legitimate IO
coroutines when multiple uploads run in parallel.

Switching to Dispatchers.Default caps concurrent conversions to the
CPU count, which is also desirable given the hardware encoder
contention that multiple simultaneous MediaCodec instances would
cause.

convertInternal remains a plain (non-suspending) function, so EGL
thread-affinity is still preserved for the lifetime of the call.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-09 09:21:30 +02:00
davotoula 564fedcac8 test: add unit tests for GIF frame-delay parser bounds checks
Covers the bounds-checking fixes from the previous commit with 13
pure-JVM unit tests (no Android dependencies, no Robolectric):

- Image Descriptor (0x2C) packed-byte offset and length precheck
- skipSubBlocks clamp against oversized block lengths
- Local and Global Color Table skipping
- Delay normalization (0 and 1 centisecond → 100 ms)
- Multi-frame parsing with variable delays
- Non-GCE extension blocks (Application Extension) skipped safely
- Truncated inputs do not crash

parseGifFrameDelays is exposed as `internal` with
@VisibleForTesting(otherwise = PRIVATE) so production callers still
see it as private.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-09 09:06:46 +02:00
davotoula e4c12ff5ad fix: harden GIF-to-MP4 converter after code review
Address issues found in Kotlin code review:

HIGH
- Rethrow CancellationException in convert() to preserve structured
  concurrency; prior broad catch silently swallowed cancellation.
- Cap drainEncoder's post-EOS loop at 500 iterations to prevent an
  unresponsive hardware encoder from hanging the IO thread forever.

MEDIUM
- Read GIF with a 20 MB size cap via bounded buffer to avoid OOM on
  malformed or adversarial inputs.
- Check glCompileShader / glLinkProgram status and throw with
  glGetShaderInfoLog / glGetProgramInfoLog on failure, instead of
  silently producing blank frames on driver errors.
- Verify numConfigs[0] > 0 after eglChooseConfig and use requireNotNull
  on configs[0] with a clear message (avoids NPE from !!).
- Fix Image Descriptor (0x2C) parsing: precheck pos + 10 <= bytes.size
  and read the packed byte at the correct offset; bounds-check after
  Local Color Table skip.
- Use uris.hasVideo() instead of uris.first().media.isVideo() for the
  privacy-toggle visibility so mixed-media selections (image + video)
  hide the toggle correctly.

LOW
- skipSubBlocks now clamps pos with minOf(pos + blockSize, bytes.size)
  to preserve the invariant that pos is always a valid index.
- Document the function-level @Suppress("deprecation") on
  convertInternal (android.graphics.Movie has no modern replacement).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-09 09:06:46 +02:00
Claude 613237bdb9 fix: hide quality slider and skip re-compression for GIF-to-MP4
The GIF converter already produces a well-compressed H.264 MP4, so
re-compressing via VideoCompressionHelper is wasteful and can inflate
file size. Hide the quality slider in the upload UI when GIF-to-MP4
is enabled and skip the video compression step in MediaCompressor.

https://claude.ai/code/session_018sKCM3VNWtfPMZSYVWWD4w
2026-04-09 09:06:46 +02:00
Claude a9978277a3 feat: add GIF to MP4 conversion option in upload screen
When a GIF is selected for upload, a new "Convert GIF to MP4" switch
appears in the upload settings dialog. This converts animated GIFs to
MP4 video using Android's Movie decoder and MediaCodec/MediaMuxer
encoder, resulting in smaller file sizes and better playback
compatibility. The converted video can optionally be further compressed
using the existing video compression pipeline.

https://claude.ai/code/session_018sKCM3VNWtfPMZSYVWWD4w
2026-04-09 09:06:46 +02:00
Claude 6e589c693c perf: eliminate ~7 allocations per signature in batch verify
The per-signature loop in verifySchnorrBatch allocated ~7 objects per
signature: 4 LongArray(4) for r/s/e/rx/ry, 1 ByteArray for hash input,
1 ByteArray(32) for sha256 output, plus ScalarN.reduce intermediates.
For a batch of 16 signatures, that's ~112 allocations.

Replace with pre-allocated scratch buffers from PointScratch:
- r, s, e → scalarTmp1/2/3
- rx, ry → entryTmp/entryTmp2
- hashInput → hashBuf (reused across iterations)
- sha256 → sha256Into with bytesTmp1
- ScalarN.reduce → ScalarN.reduceTo (in-place)
- liftX → 4-arg variant with zInv as temp

Also eliminated 2 MutablePoint allocations outside the loop by reusing
scratch (entryResult for addPoints output).

JVM batch(32): 62,949 → 82,121 ev/s (+30%)
JVM batch(16): 64,116 → 81,270 ev/s (+27%)

https://claude.ai/code/session_01EMY5RnXb9rnsyU2KbXrSaY
2026-04-09 03:33:04 +00:00
Claude e872a37e86 feat: add verifySchnorrFast for Nostr — skip y-parity inversion
BIP-340 verification checks both R.x == r AND R.y is even. The y-parity
check requires a full field inversion (~270 field ops, ~14% of verify).

verifySchnorrFast skips the y-parity check, verifying only the
x-coordinate in Jacobian coordinates (2 field ops, no inversion).

WHY THIS IS SAFE FOR NOSTR:
For a given x on secp256k1, there are exactly 2 points: (x, y_even) and
(x, y_odd). A signature producing the correct x but wrong y-parity would
require solving the discrete log — equivalent to forging the signature.
The y-parity check is defense-in-depth, not a distinct security boundary.

DO NOT use for Bitcoin/financial protocols — use verifySchnorr for strict
BIP-340 compliance.

JVM benchmark:
  verifySchnorrFast:  20,706 ops/s  (1.4× vs native C)
  verifySchnorr:      18,038 ops/s  (1.6× vs native C)
  Improvement: ~15% faster

https://claude.ai/code/session_01EMY5RnXb9rnsyU2KbXrSaY
2026-04-09 03:00:41 +00:00
Claude c915c2b883 perf: eliminate ~15 allocations per signSchnorr, ~3 per verifySchnorr
Allocation audit from Android benchmark showed 19 allocs in signSchnorr
and 4 in verifySchnorr. Most were intermediate ByteArray/LongArray that
can be replaced with pre-allocated scratch buffers.

Changes:
- Add sha256Into() (expect/actual) that writes digest into existing buffer
  instead of allocating a new ByteArray(32) per call. Uses
  MessageDigest.digest(buf,off,len) on JVM/Android, CC_SHA256 on Apple.
- Add scratch byte buffers to PointScratch: hashBuf(256), bytesTmp1/2(32),
  scalarTmp1/2/3 for intermediate scalar results.
- Add ScalarN.reduceTo() allocation-free variant.
- Rewrite signSchnorrInternal to reuse scratch buffers for:
  - dBytes serialization (bytesTmp1 instead of U256.toBytes alloc)
  - AUX_PREFIX+auxrand hash (hashBuf instead of array concatenation)
  - auxHash XOR (scalarTmp1/2/3 instead of U256.fromBytes allocs)
  - nonce/challenge hash inputs (hashBuf instead of ByteArray alloc)
  - nonce scalar (scalarTmp1 instead of ScalarN.reduce alloc)
  - challenge scalar (scalarTmp3 instead of allocs)
  - e*d and k+e*d (splitK1/entryTmp2 instead of ScalarN.mul/add allocs)
- Rewrite verifySchnorr to use hashBuf and sha256Into for challenge hash.

Only the 64-byte output signature is allocated per sign call.
Verify allocates nothing for 32-byte messages (hashBuf is large enough).

https://claude.ai/code/session_01EMY5RnXb9rnsyU2KbXrSaY
2026-04-09 02:32:09 +00:00
Claude 65652d1256 feat: add signSchnorrWithXOnlyPubKey for zero-copy cached signing
BIP-340 public keys always have even y, so the y-parity prefix byte
(0x02) is redundant when the caller already has the 32-byte x-only
pubkey. This new overload takes the x-only pubkey directly, avoiding:

1. The expensive G multiplication to derive the pubkey (~20μs on Android)
2. The 33→32 byte array copy that signSchnorrWithPubKey does internally

Added to:
- Secp256k1.signSchnorrWithXOnlyPubKey (core implementation)
- Secp256k1Instance (expect/actual, falls back to C lib's signSchnorr
  since the native C lib always derives pubkey internally)
- Secp256k1InstanceOurs (uses the optimized pure-Kotlin path)
- Nip01Crypto.signWithPubKey (app-level convenience)

The app's KeyPair already stores the 32-byte x-only pubkey. Callers
like EventAssembler.hashAndSign can pass it through to skip the
G multiplication entirely.

https://claude.ai/code/session_01EMY5RnXb9rnsyU2KbXrSaY
2026-04-09 02:17:25 +00:00
Claude 0c27ed89bc fix: align all 3 secp256k1 benchmarks for fairness and comparability
JVM benchmark fixes:
- signSchnorr(cached pk): document that native baseline still derives
  pubkey (apples-to-oranges by design — shows what caching enables)
- privKeyTweakAdd: document .copyOf() penalty on native side (ACINQ
  wrapper mutates input, requiring defensive copy)
- Remove duplicate tweakMulCompact test (redundant with ecdhXOnly)
- Add cache-warming note to benchmark KDoc
- Change output "slower" → neutral "x" (0.7x isn't "slower")

Android benchmark fixes:
- Remove misnamed pubKeyCompressOurs (was identical to compressedPubKeyForOurs)
- Remove duplicate pubKeyCompress (was also doing create+compress)
- Clean up structure: matched Native/Ours pairs with clear section headers
- Add cache-warming note to benchmark KDoc
- Standardize test data comment for cross-platform comparability

K/Native benchmark fixes:
- Fix DCE risk in micro-benchmarks: use result-dependent chains
  (out→out for field ops, xor hiSink for multiplyHigh) so LLVM can't
  hoist constant computations out of the loop
- Add batch verify (was missing — JVM had it, Android/K/N didn't)
- Add -opt documentation warning in KDoc
- Remove unused pubkeyCreate/pubKeyCompress standalone benchmarks
  (compressedPubKeyFor already covers create+compress)
- Extract printResults helper to reduce duplication

All 3 benchmarks now:
- Use the same test data (same hex keys across JVM/Android/K/N)
- Measure the same core operations (verify, sign, compressedPubKeyFor,
  secKeyVerify, privKeyTweakAdd, ecdhXOnly/pubKeyTweakMulCompact)
- Document cache-warming effects in their KDoc
- Include batch verify (JVM + K/N; Android uses framework-managed tests)

https://claude.ai/code/session_01EMY5RnXb9rnsyU2KbXrSaY
2026-04-09 01:58:54 +00:00
Claude 715e6e598c perf: add -Xno-param/call/receiver-assertions to remove null checks
Kotlin generates Intrinsics.checkNotNullParameter at the entry of every
function taking non-null reference types. Bytecode audit showed:
  Before: 128 checkNotNullParameter calls across secp256k1 classes
  After:  8 (only expression-value checks in non-hot paths)

Per Schnorr verify, this eliminates ~4,000+ invokestatic calls.
On ART (~2-3ns each): saves ~8-12μs per verify.
On HotSpot: neutral (C2 already optimizes null checks to fast branches).

These flags are safe for this module: all internal secp256k1 functions
use non-null LongArray/MutablePoint parameters that are never null.
Applied at the module level (compilerOptions) so all targets benefit.

https://claude.ai/code/session_01EMY5RnXb9rnsyU2KbXrSaY
2026-04-09 01:29:20 +00:00
Claude ec087304ad perf: Jacobian x-check + inline wNAF zero-checks in verify
Two optimizations for verifySchnorr:

1. Jacobian x-coordinate check before toAffine:
   Instead of converting to affine first (1 inversion = ~270 field ops),
   check X == r·Z² in Jacobian coordinates (1 sqr + 1 mul = 2 ops).
   Invalid signatures (x mismatch) are rejected immediately without
   paying the inversion cost. Valid signatures still need inversion for
   the y-parity check. For Nostr, ~all sigs are valid so this mainly
   helps adversarial/spam rejection.

2. Inline wNAF zero-checks in mulDoubleG inner loop:
   ~70% of wNAF digits are zero. Previously, each still called
   addWnafMixedPP (function call overhead: null checks, frame setup).
   Now the zero-check is inlined before the call, avoiding ~364
   function calls per verify. On ART (5-8ns per call), this saves
   ~2-3μs. On HotSpot, the JIT already optimized this — no change.

https://claude.ai/code/session_01EMY5RnXb9rnsyU2KbXrSaY
2026-04-09 01:29:20 +00:00
Claude 41e54ca33e perf: use unfused path on JVM (smaller methods for HotSpot inlining)
The fused fieldMulReduceWith + crossinline lambda was designed for ART
which struggles with deep call chains. On HotSpot C2, it creates a
2351-bytecode method (exceeding FreqInlineSize=325) that can't be
inlined into FieldP.mul, and wastes 180 bytecodes on lambda param
shuffling that C2 must clean up.

The unfused path (U256.mulWide + FieldP.reduceWide) produces a tiny
40-bytecode fieldMulReduce that HotSpot easily inlines. HotSpot's 8+
level inlining depth handles the full chain down to the
Math.unsignedMultiplyHigh intrinsic (single MULQ on x86-64).

Benchmark on JVM 21 x86-64: equivalent performance (1.5× verify,
0.9× sign-cached vs native C). Cleaner bytecode with no lambda waste.

https://claude.ai/code/session_01EMY5RnXb9rnsyU2KbXrSaY
2026-04-09 01:29:20 +00:00
Claude 9334994ce3 feat: add Kotlin/Native benchmark for secp256k1 on linuxX64
Mirrors the JVM benchmark (Secp256k1Benchmark.kt) for cross-platform
comparison of the same Kotlin code across runtimes:

  K/Native LLVM AOT (-opt) on x86-64:
    verifySchnorr:  101,406 ns  (9,861 ops/s)
    signSchnorr:     80,424 ns  (12,434 ops/s)
    FieldP.mul:          43 ns  (22.8M ops/s)

  JVM HotSpot C2 on same machine:
    verifySchnorr:   53,600 ns  (18,640 ops/s)
    signSchnorr:     42,400 ns  (23,518 ops/s)

  K/N vs JVM: ~1.9× slower (vs C: ~2.9×)
  K/N vs C native: ~2.9× slower

Includes field-level micro-benchmarks (FieldP.mul/sqr/add/sub/inv,
unsignedMultiplyHigh, uLt) for isolating LLVM codegen quality.

Also adds -opt to native test compilations for benchmark accuracy
(without it, debug mode is ~12× slower).

Run: ./gradlew :quartz:linuxX64Test --tests "*.Secp256k1NativeBenchmark"

https://claude.ai/code/session_01EMY5RnXb9rnsyU2KbXrSaY
2026-04-09 01:29:20 +00:00
Claude 6f3793e7ed docs: add performance rationale to secp256k1 optimization decisions
Document WHY each approach was chosen, what alternatives were tested,
and what the measured impact was — so future contributors don't
accidentally revert optimizations or repeat failed experiments.

Key decisions documented:
- uLt() expect/actual: why XOR on Android, Long.compareUnsigned on JVM,
  and why a shared inline fun in commonMain caused 30% JVM regression
- fieldMulReduceWith: why fused mul+reduce, why inline+crossinline,
  why NOT 5x52 limbs, why NOT single-method with all API branches
- @JvmField: why it's required on MutablePoint/AffinePoint/PointScratch,
  with bytecode counts showing ~7,450 + ~2,000 virtual getter calls
  eliminated per verify

https://claude.ai/code/session_01EMY5RnXb9rnsyU2KbXrSaY
2026-04-09 01:29:20 +00:00
Claude 30151295ad perf: make uLt() platform-specific to avoid JVM regression
The inline uLt() using XOR trick in commonMain regressed JVM by ~30%
because HotSpot's Long.compareUnsigned is a JIT intrinsic (single
unsigned CMP + SETB), while the XOR trick generates 2 extra XOR insns
that HotSpot doesn't optimize away.

Convert uLt to expect/actual:
- JVM: Long.compareUnsigned (HotSpot intrinsic)
- Android: XOR trick (avoids ULong.constructor-impl NOOP calls)
- Native: XOR trick (no JVM intrinsics available)

JVM verify: 2.1x → 1.5x (restored, slightly better than 1.6x baseline)
Android: unchanged (still uses XOR trick)

https://claude.ai/code/session_01EMY5RnXb9rnsyU2KbXrSaY
2026-04-09 01:29:20 +00:00
Claude aa9470e6bb perf: add @JvmField to eliminate ~7,450 virtual getter calls per verify
Bytecode analysis showed MutablePoint.x/y/z, AffinePoint.x/y, and all
PointScratch properties compile to invokevirtual getter calls instead
of direct field reads (getfield). Per verify:

  - MutablePoint getX/Y/Z: ~7,450 invokevirtual → getfield
  - PointScratch getT/getW/etc: ~2,000+ invokevirtual → getfield

Each invokevirtual has ~3-5ns overhead on ART vs ~1ns for getfield.
@JvmField eliminates the getter method entirely, compiling property
access to a direct field read. On non-JVM targets (iOS), @JvmField
is silently ignored.

https://claude.ai/code/session_01EMY5RnXb9rnsyU2KbXrSaY
2026-04-09 01:29:20 +00:00
Claude ab3a9076e3 perf: eliminate ULong.constructor-impl NOOP calls in unsigned comparisons
Bytecode analysis revealed that every `a.toULong() < b.toULong()`
comparison generates 2 invokestatic calls to ULong.constructor-impl
(NOOPs that return the input unchanged) plus Long.compareUnsigned.
Across all secp256k1 hot paths, this produced 554 NOOP invokestatic
calls in the bytecode, translating to ~18,000 wasted calls per verify.

Replace all toULong() comparisons with an inline uLt() helper that
uses the XOR-with-MIN_VALUE trick directly:

  (a xor Long.MIN_VALUE) < (b xor Long.MIN_VALUE)

This produces pure arithmetic bytecode (lxor, lcmp, ifge) with ZERO
method calls, eliminating all ULong.constructor-impl overhead.

Before: lload, invokestatic ULong.constructor-impl, lload,
        invokestatic ULong.constructor-impl, invokestatic
        Long.compareUnsigned, ifge  (6 bytecodes, 3 method calls)

After:  lload, ldc MIN_VALUE, lxor, lload, ldc MIN_VALUE, lxor,
        lcmp, ifge  (8 bytecodes, 0 method calls)

https://claude.ai/code/session_01EMY5RnXb9rnsyU2KbXrSaY
2026-04-09 01:29:20 +00:00
Claude 5add793abe perf: split Android fieldMulReduce into per-API methods for ART JIT
The previous approach inlined all 3 API-level branches into a single
fieldMulReduce method (~600 DEX instructions). ART's register allocator
produces suboptimal code for such large methods, causing stack spills
that negate the benefit of eliminating wrapper calls.

Split each API-level path into its own private function (~200 DEX
instructions each). ART JIT-compiles only the hot one (fieldMulApi35
on API 35+) with full optimization, while the tiny dispatch function
(fieldMulReduce) is easily devirtualized and inlined.

https://claude.ai/code/session_01EMY5RnXb9rnsyU2KbXrSaY
2026-04-09 01:29:20 +00:00
Claude 084230937b perf: fuse field multiply+reduce to eliminate ART wrapper overhead
On Android (ART), the unsignedMultiplyHigh wrapper function has per-call
branching for API level detection that prevents ART's JIT from inlining
the intrinsic into the hot loop. This creates ~10,000 extra function
calls per signature verify (20 wrapper calls × 500 field muls).

This commit introduces fieldMulReduce/fieldSqrReduce as expect/actual
functions that fuse U256.mulWide + FieldP.reduceWide into a single
compilation unit. The multiply-high intrinsic is passed as a crossinline
lambda and inlined at each call site, producing platform-specific code
with zero wrapper overhead:

- Android API 35+: Math.unsignedMultiplyHigh inlined directly (UMULH)
- Android API 31-34: Math.multiplyHigh + correction inlined (SMULH)
- Android API <31: pure-Kotlin fallback inlined
- JVM: Math.unsignedMultiplyHigh inlined directly
- Native: pure-Kotlin fallback inlined

The API level check happens ONCE per fieldMulReduce call (outermost
branch) rather than per multiply-high call (innermost loop), so ART
profiles and JIT-compiles only the hot path.

https://claude.ai/code/session_01EMY5RnXb9rnsyU2KbXrSaY
2026-04-09 01:29:19 +00:00
Vitor Pamplona 5cef0c1c46 updates versions 2026-04-08 21:27:14 -04:00
Vitor Pamplona f81cbfd7e4 Merge pull request #2187 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-04-08 20:00:41 -04:00
Crowdin Bot df243d4bf4 New Crowdin translations by GitHub Action 2026-04-08 23:59:08 +00:00
Vitor Pamplona d3907c10da Import function 2026-04-08 19:42:02 -04:00
Vitor Pamplona d2bb9789f5 Merge pull request #2186 from vitorpamplona/claude/add-nwc-connection-buttons-EWG8c
Add wallet connection UI controls to AddWalletScreen
2026-04-08 18:19:48 -04:00
Claude d07b8edd19 feat: add Connect Wallet, paste, and QR scan buttons to AddWalletScreen
Port the NWC connection buttons from the ZapSetup screen (UpdateZapAmountDialog)
to the AddWalletScreen used by the multi-wallet flow. Users can now connect a
local wallet app via deep link, paste an NWC URI from clipboard, or scan a QR
code — matching the same options already available in the zap settings.

https://claude.ai/code/session_01QFunqjudjjrrn6pnsUSPCS
2026-04-08 22:17:53 +00:00
Vitor Pamplona 0fa759cd58 Merge pull request #1988 from vitorpamplona/claude/multi-wallet-nwc-support-Fb04l
feat: add multi-wallet NWC support with balance view and default picker
2026-04-08 17:51:04 -04:00
Claude 55fc9c7caf feat: add wallet rename, dedicated add screen, and reorder controls
- AddWalletScreen: dedicated screen with name input + NWC URI paste,
  replacing the generic NIP47Setup flow for adding new wallets
- Rename: inline dialog on wallet cards to rename wallets
- Reorder: up/down arrow buttons on wallet cards to reorder the list
- AccountSettings: add renameNwcWallet() and moveNwcWallet() methods
- WalletViewModel: add addWallet(), renameWallet(), moveWallet()
- Fix client.send -> client.publish after main rename
- New Route.WalletAdd for the dedicated add wallet screen

https://claude.ai/code/session_013sWHKeE1uNfAZJWD4FU9mT
2026-04-08 21:46:26 +00:00
Claude d0935ff5ce feat: add multi-wallet NWC support with balance view and default picker
Migrate from a single NWC wallet connection to supporting multiple wallets.
Users can now add, remove, and manage multiple NWC wallets, view balance
and transactions per wallet, and select a default wallet for zaps.

Key changes:
- New NwcWalletEntry/NwcWalletEntryNorm data models for wallet storage
- AccountSettings: replace single zapPaymentRequest with nwcWallets list
  and defaultNwcWalletId, with backward-compatible changeZapPaymentRequest
- LocalPreferences: new persistence keys with automatic migration from
  legacy single-wallet format
- NwcSignerState: derives default wallet URI from multi-wallet settings,
  adds sendNwcRequestToWallet for targeting specific wallets
- Account: adds sendNwcRequestToWallet method
- WalletViewModel: supports wallet list, per-wallet balance/info fetching,
  wallet selection, set default, and remove operations
- WalletScreen: shows wallet cards with balance, default indicator, and
  management actions (set default, remove with confirmation)
- WalletDetailScreen: per-wallet detail view with balance, send/receive
- New Route.WalletDetail for per-wallet navigation

https://claude.ai/code/session_013sWHKeE1uNfAZJWD4FU9mT
2026-04-08 21:42:48 +00:00
Vitor Pamplona 86e4c85862 Merge pull request #2185 from vitorpamplona/claude/filter-feed-by-community-4pD5l
Remove redundant route parameter from Community FeedDefinition
2026-04-08 17:18:03 -04:00
Vitor Pamplona 58a508fcb4 Merge pull request #2183 from greenart7c3/claude/move-payment-targets-wallet-KQlnM
Integrate Lightning Address and Payment Targets into NIP47 Setup
2026-04-08 17:12:21 -04:00
Vitor Pamplona c2a1613e6f Merge pull request #2184 from vitorpamplona/claude/improve-chess-card-display-evYsA
Enhance chess UI with user profiles and player display
2026-04-08 17:11:58 -04:00
Claude b861a71c2c feat: replace hex pubkeys with user picture + name in chess cards
In chess note cards (NoteCompose), player hex keys were displayed as
raw strings. Now uses LoadUser + ClickableUserPicture + UsernameDisplay
to show proper profile pictures and display names for:
- Challenge cards (incoming/outgoing)
- Game end cards (both players)
- PGN metadata in game viewers (white/black players)

Added playerContent composable slot to PGNMetadata and ChessGameViewer
so callers can inject platform-specific user rendering.

https://claude.ai/code/session_0171mKrVEfQnNRabmT7Kv4gf
2026-04-08 20:53:56 +00:00