Commit Graph

1734 Commits

Author SHA1 Message Date
Claude f5f71220a7 fix: use proven mul_wide in mul_shift384, trace scalar_mul reduction bug
Replace mul_shift384's inline product computation with the proven mul_wide
function, eliminating the row-based carry accumulation overflow bug
(t[i+4] = carry overwrites instead of adding).

Traced the remaining scalar_mul reduction bug to its exact location:
products of two ~256-bit scalars lose exactly NC[1] = 0x4551231950B75FC4
in the second fold step. The mul_wide product and first fold are correct,
but the second fold (handling sum[4..7]) loses a carry at limb position 2.

https://claude.ai/code/session_011KVZhDcV2G7idNWEBz12GY
2026-04-11 03:34:40 +00:00
Claude ff36df55f5 fix: clean up scalar_mul, reuse field mul_wide for product computation
- Remove dead code from multiple scalar_mul reduction attempts
- Use the proven mul_wide function from field.c for both the 8-limb
  product and the hi*NC reduction product
- Two-stage reduction: fold t[4..7]*NC, then fold any remaining high part
- Export mul_wide (remove static) for cross-module use

Scalar modular reduction still has a carry issue for large intermediate
products (c2 * MINUS_B2 in GLV). The product computation (mul_wide) is
verified correct. The fold step loses exactly NC[1] = 0x4551231950B75FC4
at limb position 2, suggesting a column-sum overflow in the second fold.

https://claude.ai/code/session_011KVZhDcV2G7idNWEBz12GY
2026-04-11 03:30:16 +00:00
Claude afcdd5cb3e fix: correct GLV constants and scalar_mul modular reduction
- Fix GLV_MINUS_LAMBDA constant (d[1] and d[2] were incorrectly computed
  from Kotlin signed-to-unsigned conversion)
- Fix scalar_mul reduction: the carry from folding high limbs was silently
  dropped when the target position exceeded 4 limbs. Use proper row-based
  fold with carry propagation into higher positions
- Fix in-place gej_double aliasing: when r == p, the output overwrites the
  input during computation. Added explicit copy-on-alias

Verified working: pubkeyCreate, 2*G, (n-1)*G, ecmult for all scalar sizes.
Verify path still needs debugging (ecmult_double_g gives correct result for
simple cases but the full sign→verify round-trip has a hash/nonce mismatch).

https://claude.ai/code/session_011KVZhDcV2G7idNWEBz12GY
2026-04-11 03:19:33 +00:00
Claude 5f90f55fba fix: rewrite field arithmetic with 4x64 limbs, fix aliasing and overflow bugs
Rewrite the C secp256k1 field arithmetic from 5x52-bit to 4x64-bit limbs,
matching the Kotlin Fe4 representation. This choice was validated by the
existing Kotlin benchmarks which showed 4x64 is faster due to fewer
multiplies (16 vs 25 per field mul).

Critical bugs fixed:
- uint128 overflow: accumulating 4+ cross-products in a single uint128
  accumulator overflows (4 * 2^128 > 2^128). Switched to row-based
  schoolbook multiplication (mul_wide) which adds one product at a time
- In-place doubling aliasing: gej_double(r, r) corrupted results because
  output fields were overwritten while still being read as input. Added
  explicit copy-on-alias detection
- 5x52 constant errors: P limbs, R fold constant (0x1000003D10 vs
  0x10000003D10), and fe_negate all had wrong values for 5x52

Current status: field arithmetic fully verified, pubkey generation correct,
signing works, 2*G correct. Full verify (ecmult_double_g with large
scalars) still needs GLV/wNAF chain debugging.

https://claude.ai/code/session_011KVZhDcV2G7idNWEBz12GY
2026-04-11 03:13:51 +00:00
Claude 29b678ab14 feat: add custom C secp256k1 implementation for maximum platform performance
Add a complete C implementation of secp256k1 elliptic curve operations
alongside the existing Kotlin implementation, enabling direct comparison
and extraction of maximum performance from each platform (ARM64, x86_64).

C Implementation (quartz/src/main/c/secp256k1/):
- field.h/c: 5x52-bit limb field arithmetic with __int128 support and
  lazy reduction (12-bit headroom per limb vs Kotlin's fully-packed 4x64)
- scalar.h/c: Scalar mod n arithmetic, GLV decomposition, wNAF encoding
- point.h/c: Jacobian point operations (3M+4S double, 8M+3S mixed add),
  GLV+wNAF scalar multiplication, Strauss/Shamir dual scalar multiply,
  Montgomery batch-to-affine, precomputed G tables (wNAF-12)
- schnorr.c: BIP-340 Schnorr sign/verify/verifyFast/verifyBatch with
  pubkey decompression cache and precomputed tag hash prefixes
- sha256.c: Self-contained SHA-256 for BIP-340 tagged hashes
- secp256k1_c.h: Public API matching the Kotlin Secp256k1 object
- jni_bridge.c: JNI bridge for JVM/Android integration
- benchmark.c: Standalone C benchmark (cmake build)
- CMakeLists.txt: Build system with ARM64/x86_64 optimization flags

Kotlin Integration:
- Secp256k1InstanceC: expect/actual wrapper (commonMain/jvmMain/androidMain/nativeMain)
- Secp256k1C: JVM JNI binding class
- Secp256k1TripleBenchmark: Three-way JVM benchmark (ACINQ vs Kotlin vs Custom C)
- Secp256k1CBenchmark: Android benchmark for the C implementation

Current status: sign works correctly (verified against BIP-340 test vectors),
verify path needs ecmult_double_g debugging (GLV wNAF-12 table issue). The
comb table for ecmult_gen also needs fixing (currently falls back to GLV+wNAF).
Field arithmetic is fully verified: 5x52 limbs with R=0x1000003D10 fold.

https://claude.ai/code/session_011KVZhDcV2G7idNWEBz12GY
2026-04-11 02:36:58 +00:00
Claude fa0da7fae3 feat: upgrade negentropy-kmp to v1.0.2, enable macosArm64 target
negentropy-kmp v1.0.2 now publishes a macosArm64 artifact, unblocking
the macOS native target. Wired macosMain/macosTest source sets through
appleMain/appleTest and updated run_all.sh to run the K/Native
benchmark on both Linux (linuxX64) and macOS (macosArm64).

https://claude.ai/code/session_01WSNE6QKiYM2ZutQD2UihCW
2026-04-11 00:44:33 +00:00
Claude 731e32eb61 fix: revert macosArm64 target (blocked by negentropy-kmp), add K/N toolchain to session hook
macosArm64 target cannot be enabled until negentropy-kmp publishes a
macosArm64 artifact — commented out with explanation. Reverted run_all.sh
back to Linux-only for K/Native benchmark.

Added Kotlin/Native toolchain dependencies (GCC sysroot, LLDB, LLVM,
libffi) to session-start.sh so K/N compilation works in Claude Code
remote environments where Gradle's own downloader fails through the
proxy.

https://claude.ai/code/session_01WSNE6QKiYM2ZutQD2UihCW
2026-04-11 00:44:33 +00:00
Claude bfe0555589 fix: format C benchmark output with comma thousands and 10-digit columns
https://claude.ai/code/session_01WSNE6QKiYM2ZutQD2UihCW
2026-04-11 00:44:33 +00:00
Claude ce199f42df fix: drop deprecated macosX64 target, keep only macosArm64
macosX64 (Intel) has been removed from Kotlin's native target tiers.
Only macosArm64 (Apple Silicon) is supported.

https://claude.ai/code/session_01WSNE6QKiYM2ZutQD2UihCW
2026-04-11 00:44:33 +00:00
Claude 09001bbe98 feat: add macOS native targets (macosX64, macosArm64) to quartz
- Declare macosX64() and macosArm64() KMP targets in build.gradle.kts
- Wire macosMain/macosTest source sets through appleMain/appleTest
- Move Secp256k1NativeBenchmark from linuxX64Test to nativeTest so it
  runs on all native targets (Linux, macOS, iOS)
- Use platform() for dynamic labels instead of hardcoded "linuxX64"
- Update run_all.sh to pick the correct native target per OS/arch

https://claude.ai/code/session_01WSNE6QKiYM2ZutQD2UihCW
2026-04-11 00:44:33 +00:00
Claude dee050c6d8 fix: improve Android benchmark result discovery in run_all.sh
Three issues fixed:
- Replace fragile -newer comparison against the script file (breaks
  after any script edit) with a timestamp file created just before the
  Gradle task runs.
- Search connected_android_test_additional_output for pulled benchmark
  JSON (where AndroidX Benchmark actually writes results via Gradle).
- Extract benchmark data from XML via CDATA parsing instead of dumping
  raw XML, consistent with the JVM and K/Native sections.

https://claude.ai/code/session_01WSNE6QKiYM2ZutQD2UihCW
2026-04-11 00:44:33 +00:00
Claude 8804e243a4 fix: rewrite dylib install name on macOS so rpath resolution works
The ACINQ secp256k1-kmp-jni dylib ships with a relative LC_ID_DYLIB
("build/darwin/libsecp256k1-jni.dylib"). macOS dyld resolves this
literally, ignoring the -Wl,-rpath passed at link time, causing an
immediate abort at launch. Using install_name_tool to rewrite the
install name to @rpath/libsecp256k1-jni.dylib lets dyld find the
library via the rpath we already set.

https://claude.ai/code/session_01WSNE6QKiYM2ZutQD2UihCW
2026-04-11 00:44:32 +00:00
Claude 865c71e0e2 fix: WebRTC call bugs, hardening, camera switch, and network resilience
Bug fixes:
- Fix RemoteVideoMonitor killing group monitor job when primary track switches
- Add mutex protection to CallManager.initiateCall() to prevent state races
- Fix ICE restart offer never being sent to remote peer (was immediately
  replaced by a second offer from onRenegotiationNeeded)
- Fix duplicate duration timer in PiP connected call UI
- Fix error snackbar dismiss button not clearing the error
- Make PeerSessionManager thread-safe with synchronized blocks (accessed
  from WebRTC native threads and coroutine dispatchers concurrently)
- Make CallManager event handlers private (only called from onSignalingEvent)

Improvements:
- Replace fragile ICE candidate regex parsing with kotlinx.serialization JSON
- Respect DND/silent mode: only ring in NORMAL mode, only vibrate in VIBRATE
- Signal camera-off to remote peer by removing video track sender (instead
  of sending frozen/black frame)
- Clear CallSessionBridge on AccountViewModel.onCleared() to prevent stale
  references on account switch
- Custom TURN servers now replace defaults (instead of appending) so
  credentials can be rotated without an app update

New features:
- Front/back camera switch button (visible when video is enabled)
- Network transition handling: ConnectivityManager.NetworkCallback triggers
  ICE restart on all peers when network changes (WiFi/cellular handoff)

https://claude.ai/code/session_01JHn7skAibTrkVqsoWutgYe
2026-04-10 22:37:43 +00:00
Vitor Pamplona 8813907bd3 Merge pull request #2204 from vitorpamplona/claude/optimize-secp256k1-performance-DPixr
Replace LongArray with Fe4 struct for secp256k1 field elements
2026-04-10 10:30:45 -04:00
Vitor Pamplona c7f08a1c44 Merge pull request #2202 from vitorpamplona/claude/review-marmot-mls-nFJQr
RFC 9420 compliance: encryption, commit ordering, and thread safety
2026-04-10 08:50:22 -04:00
M ed11d2cd45 fix(quartz): snapshot LargeCache entries in forEach to prevent ConcurrentModificationException
On Apple (iOS/macOS) and Linux targets, LargeCache.forEach() iterates
the underlying map directly. When another coroutine modifies the map
during iteration (e.g., NostrClient.syncFilters running while
subscriptions are added), a ConcurrentModificationException is thrown.

On JVM/Android this is not an issue because ConcurrentSkipListMap
handles concurrent iteration safely. On Kotlin/Native (iOS), this
exception is fatal — K/N calls abort() for unhandled exceptions,
crashing the app immediately after account creation when relays
connect and subscriptions start syncing.

Fix: call .entries.toList() before iterating to create a snapshot,
matching the JVM behavior where concurrent modifications during
iteration are tolerated.
2026-04-10 20:53:19 +10:00
Claude 2bb72ac43d docs: document C interop benchmarks and K/N MUL limitation
Benchmarked two approaches for hardware 128-bit multiply on K/N:

1. Full mulWide via C interop (memScoped + allocArray + fe4_mul_reduce):
   FieldP.mul: 44ns → 116ns (2.6x SLOWER — copy/marshal overhead)

2. Per-call umulh via C interop (fe4_umulh, 20 calls per field mul):
   FieldP.mul: 44ns → 331ns (7.5x SLOWER — ~15ns bridge per call)

Conclusion: K/N cinterop bridge adds ~15ns per call, making fine-grained
C interop unviable for the multiply-high hot path (20+ calls per field op).
The pure-Kotlin fused approach (4 IMUL per 128-bit product) remains optimal
at 44ns/op until K/N supports hardware MUL natively.

Updated FieldMulPlatform.native.kt docs with benchmarked rationale.
Fixed remaining LongArray references in native benchmark test.

https://claude.ai/code/session_01Sxi6Gpxbstuj3Y8TBY7XrU
2026-04-10 08:33:58 +00:00
Claude eee8b66b53 fix: round 2 MLS security fixes — resolve critical key schedule and validation gaps
Critical fixes:
- Fix PSK/ExternalInit proposals by Reference dropped from key schedule:
  processCommit now collects ALL resolved proposals (inline + by-reference)
  into resolvedProposals list used for PSK and ExternalInit computation
- Fix decrypt() missing blank-leaf membership check: validate sender leaf
  is non-null (occupied) before proceeding with decryption

High fixes:
- Fix MlsGroupManager.decrypt() now mutex-protected to prevent concurrent
  SecretTree ratchet corruption and potential nonce reuse

Medium fixes:
- Fix externalJoin: verify GroupInfo signature before trusting tree/keys
- Fix parentHash verification: COMMIT leaf nodes must have non-empty
  parentHash (no longer silently skipped)
- Fix proposal application order: Updates/Removes applied before Adds
  per RFC 9420 §12.4.2 (frees blank slots before reuse)
- Add encryption key uniqueness check in RatchetTree.addLeaf() per §7.3
- Add LeafNode capabilities validation: verify version and ciphersuite
  support in applyProposalAdd per §12.1.1
- Remove redundant confirmation tag recomputation in processCommit

https://claude.ai/code/session_017SjKXS4Vpu4xRg9zHTgpmC
2026-04-10 07:25:34 +00:00
Claude 28b76f4320 fix: update native benchmark to use Fe4 field access
Fix a[0]/b[0] array indexing to a.l0/b.l0 in the unsignedMultiplyHigh
and uLt micro-benchmarks on Kotlin/Native linuxX64.

Native benchmark results (Kotlin/Native LLVM AOT, linuxX64):
  verifySchnorr:     85,402 ns/op  (11,709 ops/s)
  verifySchnorrFast: 77,231 ns/op  (12,948 ops/s)
  signSchnorr:       74,702 ns/op  (13,386 ops/s)
  sign (cached pk):  41,273 ns/op  (24,228 ops/s)
  pubKeyCreate:      32,305 ns/op  (30,954 ops/s)
  ecdhXOnly:        100,152 ns/op   (9,984 ops/s)

  Field micro-benchmarks:
  FieldP.mul:  44 ns/op    FieldP.add: 5 ns/op
  FieldP.sqr:  37 ns/op    FieldP.sub: 6 ns/op

  Batch verify: 3.1x (4 events) to 7.0x (32 events) faster

https://claude.ai/code/session_01Sxi6Gpxbstuj3Y8TBY7XrU
2026-04-10 07:22:35 +00:00
Claude a2ba64baba feat: migrate secp256k1 from LongArray to Fe4/Wide8 structs
Full migration of the secp256k1 library from LongArray(4)/LongArray(8)
to Fe4/Wide8 struct types with @JvmField named Long fields. This
eliminates all array bounds checks from the hot path.

Files migrated (13 source + 7 test + 2 benchmark):
  - U256.kt, FieldP.kt, ScalarN.kt, Glv.kt, ECPoint.kt
  - FieldMulPlatform.kt (expect + 3 actuals), FieldMulFused.kt
  - PointTypes.kt (MutablePoint, AffinePoint, PointScratch)
  - KeyCodec.kt, Secp256k1.kt
  - All test files and benchmarks

Bytecode impact:
  Before: 464 laload/lastore (bounds-checked) in core arithmetic
  After:  0 laload/lastore, all getfield/putfield (no checks)

The public API (Secp256k1 object) is unchanged - it still accepts
and returns ByteArray. Fe4 conversion happens at the API boundary
via U256.fromBytes()/U256.toBytes().

All secp256k1 unit tests pass on JVM.

https://claude.ai/code/session_01Sxi6Gpxbstuj3Y8TBY7XrU
2026-04-10 03:30:53 +00:00
Claude ed5515a7c5 fix: critical MLS security fixes for Marmot protocol (RFC 9420 compliance)
Phase 1 - Critical/High cryptographic fixes:
- Fix sender data nonce reuse: derive key/nonce from ciphertext sample per §6.3.1
- Add PrivateContentAAD binding (group_id, epoch, content_type) per §6.3.2
- Add SenderDataAAD binding per §6.3.1
- Fix KDFLabel encoding: use TLS fixed-width (putOpaque1/putOpaque4) not QUIC VarInt
- Add reuse_guard (4-byte random XOR into nonce) per §6.3.1
- Fix parent hash verification: capture sibling hashes before UpdatePath applied
- Fix Welcome confirmation tag: use HMAC instead of ExpandWithLabel
- Make confirmation tag mandatory in processCommit (was nullable)
- Fix off-by-one in path secret derivation during processCommit
- Fix TokenEncryption: extract 32-byte x-only pubkey from 33-byte compressed key
- Fix SELF_REMOVE proposal type: move from 0x0008 to 0xF001 (private-use range)

Phase 2 - Protocol compliance and thread safety:
- Validate KeyPackage ciphersuite is supported (0x0001 only)
- Synchronize KeyPackageRotationManager read operations with mutex
- Synchronize EpochCommitTracker with lock object
- Synchronize processedEventIds with lock in MarmotInboundProcessor
- Synchronize MlsGroupManager encrypt/decrypt with mutex
- Synchronize MarmotSubscriptionManager read methods
- Require unresolved proposal references to error per §12.4.2

Phase 3 - Hardening:
- Add path traversal validation in AndroidMlsGroupStateStore (hex-only groupId)
- Bind nostrGroupId as AAD in outer ChaCha20-Poly1305 encryption
- Track failed events in dedup set to prevent CPU exhaustion
- Validate nostrGroupId in processWelcome against Welcome event's own h tag
- Validate sender leaf index bounds during decryption

Phase 4 - Low priority fixes:
- Fix externalJoin: compute confirmedTranscriptHash and interimTranscriptHash
- Add snapshot methods for non-suspend filter access

https://claude.ai/code/session_017SjKXS4Vpu4xRg9zHTgpmC
2026-04-10 03:23:15 +00:00
Claude ea8b693785 feat: add Fe4 struct-based field elements to eliminate array bounds checks
Prototype Fe4/Wide8 classes that replace LongArray(4)/LongArray(8) with
named @JvmField Long fields. Every LongArray access compiles to
laload/lastore (3 bytecode insns + implicit bounds check); Fe4 field
access compiles to getfield/putfield (2 insns, zero checks).

Bytecode impact (core arithmetic files):
  LongArray: 464 bounds-checked array ops (U256: 150, FieldP: 119, Fused: 195)
  Fe4:         0 bounds-checked ops, 203 direct field accesses

JVM benchmark results (HotSpot C2, best of 3 rounds, alternating order):
  FieldP.mul:  40 ns → 41 ns  (~equal, C2 fully optimizes hot mul)
  FieldP.sqr:  48 ns → 30 ns  (+60% faster)
  FieldP.add:   9 ns →  6 ns  (+34% faster)
  FieldP.sub:  10 ns →  6 ns  (+55% faster)
  U256.sqrWide: 37 ns → 17 ns (+114% faster)

The gains are largest for sqr/add/sub which have high ratios of
array read-modify-write patterns. Expected to be even larger on
Android ART (limited inlining) and Kotlin/Native LLVM (AOT, no
profile-guided bounds check elimination).

https://claude.ai/code/session_01Sxi6Gpxbstuj3Y8TBY7XrU
2026-04-10 02:31:55 +00:00
Vitor Pamplona a9e59d817f Improves benchmark names, options and formatting 2026-04-09 19:25:47 -04:00
Claude 3c95229c52 fix: remove comparison table from benchmark script, keep raw results
Each platform section now prints its results directly instead of
collecting them for a final aggregated table.

https://claude.ai/code/session_01JZbyrS9xZEtJ9Y4yfsmnz1
2026-04-09 22:54:22 +00:00
Claude 141997b75d fix: support macOS in quartz benchmark run_all.sh
The script was hardcoded for Linux (searching for .so files and
linux-x86_64 paths). Add platform detection via uname so it finds
the correct native library on macOS (darwin .dylib) and Linux
(aarch64 and x86_64). Skip K/Native benchmark on non-Linux since
only linuxX64 target exists.

https://claude.ai/code/session_01JZbyrS9xZEtJ9Y4yfsmnz1
2026-04-09 22:37:52 +00:00
Claude c33e3bd54f feat: add cross-platform benchmark comparison script
run_all.sh runs all secp256k1 benchmarks and produces a formatted
comparison table. Runs C native, Kotlin/Native, JVM (always), and
Android (only if device/emulator connected via adb).

Output includes:
- ops/sec with comma-separated numbers
- libsecp256k1 vs Quartz column headers
- verifySchnorrFast and signSchnorr (cached pk) as Quartz-only rows
- Ratio table: C vs K/Native, JNI vs JVM Kotlin (apples-to-apples)
- Android column and ratios when device is connected

Run from repo root: ./quartz/benchmarks/run_all.sh

https://claude.ai/code/session_015CtM5k88rF7WFgX8o2AGNR
2026-04-09 22:10:19 +00:00
Claude e2725718c2 docs: add standalone C benchmark for libsecp256k1
Standalone C program that links against the ACINQ secp256k1-kmp-jni
.so to benchmark raw C libsecp256k1 performance without any JVM, JNI,
or ART overhead. Uses the same test vectors as the Kotlin benchmarks.

Useful as a baseline when comparing Kotlin/Native or Android results
against the C library on the same hardware.

https://claude.ai/code/session_015CtM5k88rF7WFgX8o2AGNR
2026-04-09 21:39:23 +00:00
Claude 9aa3e211b9 perf: pass PointScratch through mul and mulG, eliminate ThreadLocal
Add optional PointScratch parameter to ECPoint.mul and ECPoint.mulG
(default to scratch.get() for backward compat). All callers in
Secp256k1.kt now pass their already-fetched scratch through.

From the ECDH trace: ECPoint.mul was calling scratch.get() (ThreadLocal)
redundantly — the caller already had the scratch. On ART, each
ThreadLocal.get costs ~6µs (hash table probe), and ECDH had 10 calls
totaling 64µs.

With this change, all hot-path EC operations (verify, sign, ECDH,
pubkey create, tweak mul) fetch the scratch once at the entry point
and pass it through the entire call chain.

https://claude.ai/code/session_015CtM5k88rF7WFgX8o2AGNR
2026-04-09 21:33:38 +00:00
Claude e148ff1ed7 perf: replace all uLt with uLtInline, inline isInfinity body
Replace every non-inline uLt() call with uLtInline() across FieldP.kt,
U256.kt, and ScalarN.kt. The expect/actual uLt() can't be inline
(KMP limitation), costing ~84ns per call on ART as a real function
dispatch. From the trace: 12,394 uLt calls × 84ns = 1.035ms per
verify (1.2% of total).

uLtInline uses the same XOR-with-MIN_VALUE trick but as a package-level
inline function — zero dispatch overhead.

Also inline isInfinity() body directly: was delegating to U256.isZero()
(double dispatch), now computes (z[0] or z[1] or z[2] or z[3]) == 0L
directly. 190 calls × 347ns = 66µs saved.

https://claude.ai/code/session_015CtM5k88rF7WFgX8o2AGNR
2026-04-09 21:19:52 +00:00
Claude 40cb0e270c perf: eliminate redundant ThreadLocal.get and copyInto$default calls
Pass PointScratch through mulDoubleG instead of re-fetching from
ThreadLocal. The verify path was calling ThreadLocal.get() 2x:
once in verifySchnorrFast and again in mulDoubleG. On ART, each
ThreadLocal.get costs ~41µs (hash table probe), totaling ~83µs
per verify (~1% of total).

mulDoubleG now accepts an optional PointScratch parameter
(defaults to scratch.get() for backward compat). The verify
path passes its already-fetched scratch through.

Also fix all remaining LongArray copyInto calls with default params:
- MutablePoint.copyFrom: 3 calls per copy (x, y, z)
- MutablePoint.setAffine: 2 calls (x, y)
- mulDoubleG P-table build: 2 calls per table entry
- mul P-table build: 2 calls per table entry
- batchToAffine: 2 calls
- U256.copyInto: was delegating with defaults

Each copyInto$default adds a bitmask check + 3 branches + arraylength
per call. With ~13 LongArray copies per verify, this eliminates ~52
extra branch instructions from the hot path.

https://claude.ai/code/session_015CtM5k88rF7WFgX8o2AGNR
2026-04-09 21:14:57 +00:00
Claude cb486778db perf: eliminate heap allocations in key operations
Replace U256.fromBytes (allocates LongArray(4)) with
U256.fromBytesInto (writes to pre-allocated scratch) across
all key operations:

- pubkeyCreate: scalar → sc.scalarTmp1 (safe: mulG doesn't use it)
- signSchnorrWithPubKey: d0 → sc.zInv (safe: signSchnorrInternal
  doesn't use it; d0 is read at lines 322-324 and 391 before zInv
  is needed)
- signSchnorrWithXOnlyPubKey: same pattern with sc.zInv
- pubKeyTweakMul: scalar → sc.scalarTmp1 (safe: ECPoint.mul
  doesn't use scalarTmp fields)
- ecdhXOnly: k → sc.scalarTmp1 (same)
- privKeyTweakAdd: already fixed in previous commit

signSchnorr keeps its allocation because d0 must survive through
mulG (which destroys splitK*) AND signSchnorrInternal (which uses
scalarTmp*). The single LongArray(4) alloc is negligible vs mulG
cost (~100μs).

Each eliminated allocation saves ~10-20ns of GC pressure on ART
and ~5ns on K/Native. For signSchnorrWithPubKey (the cached-pk
fast path), this removes the only unnecessary allocation.

https://claude.ai/code/session_015CtM5k88rF7WFgX8o2AGNR
2026-04-09 21:08:35 +00:00
Claude 6b1bc947a9 perf: eliminate heap allocations in privKeyTweakAdd
Use thread-local scratch LongArrays instead of allocating 2
intermediate LongArray(4) via U256.fromBytes. The old path did:
  fromBytes(seckey) → alloc LongArray(4)
  fromBytes(tweak)  → alloc LongArray(4)
  ScalarN.add(a, b) → alloc LongArray(4)
  U256.toBytes(r)   → alloc ByteArray(32)
  = 4 heap allocations

New path uses pre-allocated scratch from PointScratch:
  fromBytesInto(scratch, seckey) → zero alloc
  fromBytesInto(scratch, tweak) → zero alloc
  ScalarN.addTo(scratch, a, b)  → zero alloc
  U256.toBytes(r)               → 1 alloc (unavoidable, return value)

K/Native: 163 → 88 ns (-46%, from 6.3x to 3.4x vs C)
JVM: now faster than native C via JNI (Kotlin 6.5M ops/s vs C 4.8M ops/s)

https://claude.ai/code/session_015CtM5k88rF7WFgX8o2AGNR
2026-04-09 20:59:58 +00:00
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
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
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
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