Commit Graph

1977 Commits

Author SHA1 Message Date
Claude c04d3d0bf0 fix(marmot): use GCMParameterSpec for decrypt + stop auto-deleting groups on restore failure
Logs from the real device showed that with the previous fixes save and
listGroups now work correctly (878 → 1162 bytes written, file found
after restart), but decrypt was throwing:

    InvalidAlgorithmParameterException: Only GCMParameterSpec supported
        at AndroidKeyStoreAuthenticatedAESCipherSpi$GCM.initAlgorithmSpecificParameters

AndroidKeyStore's authenticated AES/GCM cipher rejects plain
IvParameterSpec and requires an explicit GCMParameterSpec with the
auth tag length. Switched decrypt() to use GCMParameterSpec(128, iv).

While chasing this I also noticed that MlsGroupManager.restoreAll
was **deleting** the stored group on any exception from load(), on
the assumption that it must be corrupted. This turned a transient
decrypt bug into permanent data loss — the user's group file was
wiped during the first broken restart. Change the catch block to
log and skip instead of delete, so after a fix the next restart
can still recover the data.

https://claude.ai/code/session_014EKS8JBwSpap34aM6FnYLm
2026-04-15 19:27:36 +00:00
Claude 7f2d44df7e fix(marmot): add diagnostic logs to group persistence path + use explicit AndroidKeyStore provider
User-reported issue: creating a Marmot group works in a single session,
but the group is gone after app restart. The store code path survives
the persistGroup save on creation, but something between save and
restore silently loses the state.

Changes:

- KeyStoreEncryption: pass "AndroidKeyStore" explicitly to
  KeyGenerator.getInstance so generator.init(KeyGenParameterSpec) is
  guaranteed to land on the AndroidKeyStore provider (the default
  provider selection is not guaranteed to choose it). Also wrap
  encrypt/decrypt in try/catch so the real exception is logged
  instead of bubbling up as a generic failure.

- AndroidMlsGroupStateStore: log rootDir at construction, and log
  every save/load/listGroups/delete with file paths, byte counts,
  and whether the target file actually exists after writing. Any
  thrown exception is now logged with full stack trace before
  rethrowing.

- MlsGroupManager: log create / persist / restoreAll with group ids,
  byte counts, and in-memory group count so we can see exactly which
  step drops state. The existing "corrupted state, delete" branch now
  logs at ERROR with a clearer marker so it's easy to spot in the logs.

- MarmotManager + AccountCacheState: log which MLS store implementation
  is chosen per account (Android vs InMemory fallback) and the root
  filesDir path, plus restoreAll() begin/end with restored group ids.

These logs should make it obvious whether:
  1. persistence is silently falling back to InMemoryMlsGroupStateStore,
  2. save is actually writing the file to disk,
  3. listGroups is finding the directory on restart,
  4. decrypt is throwing and the restoreAll catch block is wiping
     the "corrupted" state.

https://claude.ai/code/session_014EKS8JBwSpap34aM6FnYLm
2026-04-15 18:24:58 +00:00
Vitor Pamplona db18954d91 Fixes key creation for groups. 2026-04-15 12:04:38 -04:00
Claude 78a1c3cb28 fix: avoid AndroidKeyStore provider for Ed25519 in Marmot MLS
On Android, KeyPairGenerator.getInstance("Ed25519") could resolve to
AndroidKeyStoreKeyPairGeneratorSpi, which rejects NamedParameterSpec and
requires KeyGenParameterSpec, crashing group creation with
IllegalArgumentException. Explicitly select a non-AndroidKeyStore
provider (Conscrypt on Android, SunEC on JVM) for KeyPairGenerator,
KeyFactory, and Signature, and drop the unnecessary initialize() call
since Ed25519 is fully specified by its algorithm name.
2026-04-15 14:25:38 +00:00
Vitor Pamplona de94b28fc2 Spotless 2026-04-14 22:24:55 -04:00
Claude ccba463686 fix(ble): migrate AndroidBleTransport off deprecated BLE APIs
Replaces deprecated BluetoothGattCharacteristic.value, writeCharacteristic,
notifyCharacteristicChanged, writeDescriptor and connectGatt overloads with
their Tiramisu (API 33+) replacements, falling back to the legacy API with
@Suppress("DEPRECATION") when running on older devices. Adds the new
onCharacteristicChanged(gatt, characteristic, value) overload and caches the
last notified payload so GATT read requests no longer read from the
deprecated characteristic.value field.
2026-04-15 02:16:15 +00:00
Claude 552ff7a2ce fix: replace JVM-only synchronized with Mutex in quartz commonMain
MarmotInboundProcessor and CommitOrdering.EpochCommitTracker used
`synchronized(lock) { ... }`, which is a JVM-only intrinsic. Compiling
the quartz KMP module for iosSimulatorArm64 (and other non-JVM targets)
failed with "Unresolved reference 'synchronized'".

Switch to kotlinx.coroutines.sync.Mutex + withLock, matching the pattern
already used in MlsGroupManager. EpochCommitTracker's public API becomes
suspend — update the MarmotInboundProcessor delegates (pendingCommitGroupEpochs,
clearPendingCommits) and wrap the commonTest cases in runTest. The
MarmotPipelineTest jvmAndroid tests already run inside runBlocking, so
no changes needed there.

Also reshape the processGroupEvent dedup check to hoist the "already
processed?" read out of the lock block so the early return isn't a
non-local return from the withLock lambda.
2026-04-15 02:02:05 +00:00
Vitor Pamplona ed245ada57 Merge pull request #2388 from vitorpamplona/claude/fix-call-activity-lifecycle-ApVaS
Add per-peer invite timeouts and calls enable/disable setting
2026-04-14 21:34:00 -04:00
Claude b9d6cefbeb feat(call): per-peer 30s invite timeout and per-peer status in video grid
Two changes that tighten how the caller-side UX handles slow or
unresponsive callees in a group call:

1. Per-peer 30-second invite timeout

   Previously the caller used a single 60-second call-wide timeout: if
   *any* peer answered within the window the timer was cancelled and
   slow peers could remain "ringing" indefinitely. For a mid-call
   invite there was no timeout at all — the invitee stayed in
   pendingPeerPubKeys forever, burning a PeerConnection on the caller
   side and keeping the invitee's device ringing.

   CallManager now schedules an independent 30-second timer for every
   peer in pendingPeerPubKeys (or Offering.peerPubKeys in the initial
   offer phase). The timer is started when the peer is added to
   pending — in beginOffering, initiateCall and invitePeer — and is
   cancelled when the peer answers (onCallAnswered), rejects
   (onCallRejected) or hangs up (onPeerHangup). On expiry the peer is
   dropped from the group, a CallHangup is published to them so their
   device stops ringing, and onPeerLeft fires so CallController can
   dispose the per-peer PeerConnection. If the drop leaves the caller
   with zero connected and zero pending peers, the call ends with
   EndReason.TIMEOUT.

   Callee ringing still uses the 60-second CALL_TIMEOUT_MS in
   onIncomingCallEvent; the two timers now serve distinct roles.

   New constant CallManager.PEER_INVITE_TIMEOUT_MS = 30_000L.

   NIP-AC.md "Event Lifecycle" documents both timers.

2. Per-peer "Calling..." status in the video grid

   The shared "Waiting for others to join…" banner across the top of
   ConnectedCallUI is removed. PeerVideoGrid now takes a
   pendingPeerPubKeys set and, for each peer still pending, routes
   rendering through PeerAvatarCell with a "Calling…" status line
   under the username — so it's obvious *which* participants the call
   is waiting on, not just *that* it's waiting. A peer in pending
   never shows as a video cell even if a stale track is still in the
   map, because they haven't actually answered yet.

   The voice-only path in ConnectedCallUI now also uses PeerVideoGrid
   (with empty tracks/active-video) instead of the single
   GroupCallPictures + GroupCallNames stack, so the per-peer status
   behavior is consistent for audio calls.

Tests added in CallManagerTest:
- perPeerTimeoutEndsP2PCallWhenBobNeverAnswers
- perPeerTimeoutDropsSlowCalleeFromGroupCall
- perPeerTimeoutIsCancelledOnAnswer
- perPeerTimeoutDropsMidCallInviteeWhenNoAnswer
- perPeerTimeoutIsCancelledOnReject
- perPeerTimeoutEndsCallWhenAllCalleesIgnore

All existing CallManagerTest cases still pass unchanged.

https://claude.ai/code/session_01XSPDbahLwHs9sdF5XfRvHB
2026-04-14 23:23:42 +00:00
Claude d087e1ac13 feat(call): full-mesh setup for mid-call invites
When an existing group call participant invites a new peer via
CallController.invitePeer(), the caller connects to the invitee via the
normal offer/answer flow, but the other existing callees were never
establishing their own PeerConnections to the new peer — breaking the
full-mesh invariant.

Root cause: the NIP-AC "callee-to-callee mesh setup" mechanism relies on
each callee observing the others' CallAnswers during its own ringing
window and applying a symmetric "lower pubkey initiates" tiebreaker.
A mid-call invitee's ringing window happens after all existing callees
have already answered, so the invitee's discoveredCalleePeers set is
empty. Worse, if the invitee's pubkey is higher than an existing
callee's, that callee also stays passive (waiting for the invitee to
initiate), and no mesh connection is ever attempted between them.

Fix — break the symmetry on mid-call invites:

- CallManager.onCallAnswered now expands peerPubKeys when an answer
  arrives in Connected (or Connecting) state from a peer that is not
  yet in the tracked group membership. This keeps the UI and state
  consistent with the expanded group and gives CallController a clear
  hook via onAnswerReceived.

- CallController.onCallAnswerReceived splits the NO_SESSION case:
    * Connected state → mid-call invite. Unconditionally initiate a
      mesh CallOffer to the new peer. The invitee stays passive, so
      exactly one side initiates per pair and glare is structurally
      impossible.
    * Connecting state → initial-call mesh observation. Keep the
      existing lower-pubkey tiebreaker via onNewPeerInGroupCall to
      avoid glare with the symmetric peer.

This fix uses the existing event kinds (CallOffer 25050 and CallAnswer
25051) — no new kinds or tags are required. NIP-AC.md is updated with
a new "Mid-Call Mesh Expansion" subsection under "Inviting New Peers"
documenting the asymmetric rule and a full-flow diagram.

Tests in CallManagerTest cover:
- Mid-call invitee's broadcast answer expands existing callee's
  peerPubKeys in Connected state.
- Same expansion works in Connecting state (existing callee still
  handshaking with caller when the invitee joins).
- Initial-call answers from already-tracked peers do NOT trigger the
  expansion branch (regression guard).
- Full end-to-end 3-way flow exercising Alice, Bob, Carol managers.

https://claude.ai/code/session_01XSPDbahLwHs9sdF5XfRvHB
2026-04-14 22:54:07 +00:00
Claude 8390badadc perf(secp256k1/c): keep fe_sqr on fe_mul_asm for FE_MUL_ASM platforms
Measurement via bench_vs_acinq (native C-to-C comparison against ACINQ's
libsecp256k1) revealed that fe_sqr_inline's 10-mul __int128 path regressed
verify by ~10% and batch verify by ~25% on x86_64 GCC+BMI2+ADX. The ASM
fe_mul_asm uses MULX plus dual ADCX/ADOX carry chains that the compiler
cannot reproduce from __int128 arithmetic, so the extra 6 multiplications
are cheaper than losing the ASM's scheduling wins.

Restore fe_sqr to fe_mul_asm(r, a, a) whenever FE_MUL_ASM is set, and
document the measurement in the fe_sqr_inline comment block. The 10-mul
inline still wins for portable builds (clang without the GCC asm blocks,
MSVC, non-x86_64/ARM64 targets), where fe_mul falls back to the generic
__int128 row-schoolbook path and dedicated squaring genuinely cuts muls.

Baseline (HEAD~1) vs branch with this fix, averaged over 3 runs of
`bench_vs_acinq` on x86_64 (µs/op, lower is better):

  Operation          ACINQ  baseline  with fix
  pubkeyCreate        17.4     14.5     14.4
  sign (derive pk)    35.7     28.8     28.2
  sign (cached pk)    18.7     14.3     14.3
  verify (BIP340)     35.0     36.4     36.2
  verifyFast          35.0     33.0     32.4
  ECDH                35.8     31.6     31.5
  batch(32)           35.4      5.6      5.6
  batch(200)          36.5      4.9      4.7

Every measurement is within run-to-run noise of baseline, and our custom
C is ~1.2-1.3x faster than ACINQ on sign/pubkey and ~6-8x faster on batch
verify (which ACINQ has no public API for at all). Correctness verified:
`Cross-verification: ACINQ verifies ours=1, We verify ACINQ=1`, and all
188 Kotlin secp256k1 tests still pass.

https://claude.ai/code/session_01KExJURZATpL59ZKXP6AVP6
2026-04-14 22:48:56 +00:00
Claude 566a0997a2 fix(secp256k1): harden C/Kotlin field and scalar arithmetic; add dedicated fe_sqr
Correctness (C):
- scalar_mul: rewrite with a loop-driven fold so the 512-bit product is
  fully reduced regardless of the pre-existing third-fold carry-drop bug;
  also fixes the portable (!HAVE_INT128) fallback which was returning
  (a*b) mod 2^256 instead of (a*b) mod n.
- fe_mul / reduce_wide: loop the final carry fold in a while(carry) rather
  than a single if(carry), so a secondary carry-out is never silently
  dropped for adversarial or deeply lazy-reduced inputs.
- scalar_add: reuse the precomputed SCALAR_NC constant instead of
  recomputing n's two's-complement arithmetically each call.
- fe_negate: remove the data-dependent early-return for a == 0; compute
  P - a unconditionally and fold P back to 0 with a final fe_normalize,
  matching the Kotlin FieldP.neg path and dropping a branch.

Performance (C):
- Add a dedicated 10-mul fe_sqr_inline using __int128 (4 diagonal + 6
  doubled cross products, three-pass structure mirroring Kotlin
  U256.sqrWide). The previous fe_sqr delegated to fe_mul(a, a) using all
  16 schoolbook products; the new path saves ~37% of the multiplications
  at every squaring, and doublePoint/addPoints do ~9 sqrs each in the hot
  Jacobian loop. Col-3 mixes two products so the accumulator is split
  explicitly to avoid a uint128 overflow; the bug was caught by the C
  benchmark's self-test.
- Enable LTO (CMAKE_INTERPROCEDURAL_OPTIMIZATION) when the toolchain
  supports it, recovering cross-TU inlining of fe_mul/fe_sqr into point.c
  and schnorr.c.
- jni_bridge: replace the pinning GetByteArrayElements path (which blocks
  GC compaction) with a stack-or-heap copy_msg_bytes helper that uses
  GetByteArrayRegion. Short messages (32 B event digests, the common case
  for Nostr) hit a 512 B on-stack buffer.

Correctness (Kotlin):
- FieldP.neg: remove the early-return on zero for the same reasons as the
  C side, with a trailing reduceSelf(out) to collapse the P result back
  to 0 when the input was zero.
- Secp256k1.signSchnorrInternal / privKeyTweakAdd: switch the crypto-
  edge-case failure modes from generic require() to check() with clear
  messages, documenting them as invariants rather than argument errors
  and matching the C side's return-code semantics.

Tests & benchmarks:
- Add Secp256k1CrossValidationTest (jvmTest) that byte-for-byte compares
  Kotlin, ACINQ, and the custom C implementation across pubkey creation,
  Schnorr signing (incl. variable message lengths on the Kotlin side),
  privKeyTweakAdd, and x-only ECDH. This is the strongest parity check we
  can run without a third reference, and it's deterministic for
  reproducibility (fixed LCG seed).
- Add adversarial FieldPTest cases that chain lazy adds into mul/sqr/inv
  to exercise the new fe_mul fold loop and the dedicated fe_sqr path.
- Fix pre-existing FieldPTest/GlvTest failures (addNearP, addNegIsZero,
  halfOfOdd, invMulIsOne, invOfTwo, reduceWideWithMaxValues, betaCubedIsOne)
  that were asserting on raw limbs of lazy-reduced values; they now
  reduceSelf before comparing, consistent with the rest of the suite.
- Secp256k1Benchmark: document the intentional apples-to-apples
  asymmetries (signSchnorrWithPubKey vs signSchnorr, ecdhXOnly vs
  pubKeyTweakMul, privKeyTweakAdd's copyOf() penalty) and add a
  taggedHash benchmark since NIP-44 leans heavily on it.

All 188 secp256k1 Kotlin tests pass; the C library builds cleanly with
LTO enabled and the secp256k1_bench self-verification succeeds.

https://claude.ai/code/session_01KExJURZATpL59ZKXP6AVP6
2026-04-12 19:58:34 +00:00
Vitor Pamplona 7d5a2b2016 Merge pull request #2269 from vitorpamplona/claude/fix-relay-connectivity-pJ29t
Improve Tor bootstrap resilience and WebSocket connection handling
2026-04-12 14:16:34 -04:00
Claude 3ae1fb36d2 perf: inline fe_mul into point operations — 10% faster verify
Move fe_mul/fe_sqr to static inline in field.h when ASM is available
(FE_MUL_ASM=1). This allows the compiler to inline the entire field
multiply directly into gej_double and gej_add_ge, eliminating function
call boundaries.

Before: gej_double had 9 function calls (to fe_mul/fe_sqr)
After:  gej_double has 2 function calls (fe_half only)

The compiler can now:
- Keep intermediate results in registers across multiply boundaries
- Schedule MULX instructions across adjacent field operations
- Eliminate push/pop register saves at call boundaries

gej_double: 738 → 1311 instructions (larger but no call overhead)

Impact:
  verifyFast: 35.1µs → 31.6µs (10% faster, 1.19x vs ACINQ)
  verify:     39.7µs → 38.4µs (0.98x vs ACINQ — essentially tied!)
  sign:       15.2µs → 14.2µs (1.48x vs ACINQ)
  batch(200): 6.2µs → 4.5µs per event (7.9x vs ACINQ)

https://claude.ai/code/session_011KVZhDcV2G7idNWEBz12GY
2026-04-12 03:02:52 +00:00
Claude 854bf9379a perf: fe_sqr calls fe_mul — eliminates 5ns/sqr gap, 33% faster fe_inv
fe_sqr was 20.6ns (using mul_wide + reduce_wide as separate functions)
while fe_mul was 15.6ns (inlined). Simply making fe_sqr call fe_mul
eliminates the gap.

This has a massive impact on fe_inv/fe_sqrt which do 255 squarings:
  fe_inv:  6107ns → 4085ns (33% faster!)
  fe_sqr:  20.6ns → 15.8ns (23% faster)
  fe_mul:  15.6ns → 14.9ns (stable)

Impact on operations:
  sign (cached):     15.2µs → 13.6µs (1.30x faster than ACINQ)
  pubkeyCreate:      15.3µs → 14.1µs (1.24x faster)
  verifyFast:        35.1µs → 32.2µs (1.01x vs ACINQ — tied!)
  verify (BIP-340):  39.7µs → 36.5µs (0.89x vs ACINQ)
  batch(200)/event:   6.2µs →  4.5µs (8.3x faster than ACINQ!)

https://claude.ai/code/session_011KVZhDcV2G7idNWEBz12GY
2026-04-12 02:52:51 +00:00
Claude 070affb4e4 fix: restore accidentally deleted test files
https://claude.ai/code/session_011KVZhDcV2G7idNWEBz12GY
2026-04-12 01:40:40 +00:00
Claude 4dea4b5db5 perf: lazy fe_mul — remove normalize from mul/sqr output (both C and Kotlin)
Remove fe_normalize/reduceSelf from the end of field multiply and
square. After reduceWide, the output is in [0, 2^256) which may
include values in [P, P+C) where C = 2^32+977. This is the same
"unreduced" range that lazy fe_add produces, and is safe because:

- mul/sqr: mulWide handles any 256-bit input via reduceWide ✓
- add: carry fold handles overflow past 2^256 ✓
- sub: P-add-back on underflow produces correct field element ✓
- neg/half: already normalize input via reduceSelf ✓
- isZero/cmp/toBytes: caller normalizes before use ✓

Native C-to-C results (x86_64, vs ACINQ):
  verifyFast: 0.95x → 0.99x (essentially tied with ACINQ!)
  sign (cached): 1.18x → 1.24x faster
  ECDH: 1.05x → 1.06x faster
  batch(200)/event: 7.2µs → 6.4µs

Kotlin JVM results (vs previous lazy-add-only):
  Kotlin numbers stable — reduceSelf in reduceWide was already
  cheap on JVM since the branch is almost never taken.

https://claude.ai/code/session_011KVZhDcV2G7idNWEBz12GY
2026-04-12 01:40:15 +00:00
Claude 8291987145 docs: document why lazy fe_sub is NOT safe with 4x64 limbs
Analyzed lazy fe_sub: on underflow, the unsigned-wrapped value
(a - b + 2^256) differs from (a - b + P) by C = 2^32 + 977.
When multiplied: (a-b+2^256)*x ≠ (a-b)*x mod p (off by x*C mod p).
The P-add-back on underflow is mandatory for correctness.

This is a fundamental difference from 5x52 lazy reduction where
magnitude tracking keeps values representable. With 4x64 fully-packed
limbs, sub MUST add P back, but add CAN skip reduceSelf since values
in [P, 2^256) differ from [0, C) which is handled by mulWide+reduceWide.

Also evaluated:
- WINDOW_G 12→14: only 1.2µs savings for 4x memory (128→512KB).
  Not worth it on phones where L1 cache is 128-256KB.
- fe_sqrt optimization: only 831ns overhead over theoretical minimum
  of 5.85µs. The addition chain is already near-optimal.

https://claude.ai/code/session_011KVZhDcV2G7idNWEBz12GY
2026-04-12 01:32:21 +00:00
Claude ae77935625 perf: lazy field addition in Kotlin — 10-16% faster across all operations
Remove reduceSelf from FieldP.add, making it lazy (same approach as
the C implementation). Values may be in [0, 2^256) between additions.

Safety analysis:
- mul/sqr: reduceWide handles any 256-bit input ✓
- neg: added reduceSelf before P - a (prevents underflow) ✓
- half: added reduceSelf before conditional add P ✓
- sub: already adds P back on borrow (self-normalizing) ✓
- isZero/cmp: called on sub outputs (normalized) or after mul ✓
- verifySchnorrCore: added reduceSelf before Jacobian x-check ✓
- toBytes: only called on toAffine outputs (from mul, normalized) ✓

JVM benchmark results (ops/sec, HotSpot C2):
  pubkeyCreate:  32,096 → 37,211 (+15.9%)
  signXOnly:     31,149 → 34,507 (+10.8%)
  sign:          16,137 → 17,822 (+10.4%)
  verify:        12,733 → 14,027 (+10.2%)
  verifyFast:    14,734 → 15,449 (+4.9%)
  ECDH:          10,975 → 12,102 (+10.3%)
  batch(200):    91,611 → 102,354 (+11.7%)

The improvement is larger than expected (10-16% vs estimated 6%)
because HotSpot's branch prediction overhead for reduceSelf
(virtual dispatch + 4 Long comparisons) is higher than the
~2.5ns estimated for native code.

https://claude.ai/code/session_011KVZhDcV2G7idNWEBz12GY
2026-04-11 23:59:44 +00:00
Claude e474f9b36f fix: add missing #include sha256.h in jni_bridge.c
https://claude.ai/code/session_011KVZhDcV2G7idNWEBz12GY
2026-04-11 15:11:05 +00:00
Claude 944562ddc8 feat: add SHA-256 benchmark comparing Android native vs our C vs Kotlin
Three SHA-256 implementations benchmarked on the same phone:
- sha256Android: Android's MessageDigest (BoringSSL + ARM64 CE hardware)
- sha256OurC: Our C SHA-256 (ARM64 CE hardware via JNI)
- sha256Kotlin: Kotlin's sha256 (platform MessageDigest on Android)

Also add nativeSha256 JNI method to expose our hardware SHA-256.

Why our C matches ACINQ on ARM64 despite different architectures:
- Our 4x64: 16 MUL+UMULH pairs per field mul
- ACINQ 5x52: 25 MUL+UMULH pairs per field mul
- We save 9 multiply pairs (~27ns on Cortex-X3) per fe_mul
- ACINQ saves ~6 normalizations with lazy reduction (~6ns)
- Net advantage: ~21ns per mul × 500 muls/verify > 0
- Plus our SHA-256 uses hardware CE, ACINQ's is software

https://claude.ai/code/session_011KVZhDcV2G7idNWEBz12GY
2026-04-11 15:02:27 +00:00
Claude 61e8471c37 fix: add missing #include <stdlib.h> in jni_bridge.c for malloc/free
https://claude.ai/code/session_011KVZhDcV2G7idNWEBz12GY
2026-04-11 14:49:04 +00:00
Claude 40a51d855c feat: add Android cross-compile script for phone benchmarking
Add build_android.sh that cross-compiles the C secp256k1 library for
ARM64 using the Android NDK and places the .so in benchmark/src/main/
jniLibs/ where the benchmark APK will package it.

Usage:
  cd quartz/src/main/c/secp256k1
  ./build_android.sh
  cd ../../../../..
  ./gradlew :benchmark:connectedAndroidTest

Also:
- Fix Secp256k1InstanceC.android.kt to use Secp256k1C JNI binding
  class (same pattern as JVM) so the JNI method names match
  Java_com_vitorpamplona_quartz_utils_Secp256k1C_native*
- Add benchmark/src/main/jniLibs/ to .gitignore

https://claude.ai/code/session_011KVZhDcV2G7idNWEBz12GY
2026-04-11 14:40:31 +00:00
Claude eb9527d793 perf: hardware SHA-256 acceleration (SHA-NI x86_64, CE ARM64)
Add hardware-accelerated SHA-256 using platform crypto extensions:

x86_64 (SHA-NI, Intel Goldmont+/AMD Zen+):
  SHA256RNDS2 — processes 2 SHA-256 rounds per instruction
  SHA256MSG1/SHA256MSG2 — message schedule expansion
  Result: SHA-256(160B) 749ns → 165ns (4.5x faster)

ARM64 (Crypto Extensions, all Android ARMv8 phones):
  SHA256H/SHA256H2 — hash update (4 rounds per instruction)
  SHA256SU0/SHA256SU1 — message schedule
  Expected: similar 4-5x speedup on phone

Impact on secp256k1 operations:
  signSchnorr:    33→31µs (8%, 3-4 SHA-256 calls per sign)
  signXOnly:      17→16µs (8%)
  verifyFast:     34→33µs (4%, 1 SHA-256 call)
  batch(200):     7.2→6.6µs/event (10%, 200 SHA-256 calls)
  batch(200) now 5.1x faster than ACINQ individual verify

Native C-to-C (with SHA-NI + all optimizations):
  pubkeyCreate:  ACINQ 15.9  Ours 14.9  1.07x faster
  sign (cached): ACINQ 17.7  Ours 15.1  1.18x faster
  sign (full):   ACINQ 33.2  Ours 30.6  1.08x faster
  verifyFast:    ACINQ 32.1  Ours 33.7  0.95x (tied)
  batch(200):    ACINQ 33.5  Ours  6.6  5.1x faster

https://claude.ai/code/session_011KVZhDcV2G7idNWEBz12GY
2026-04-11 13:19:46 +00:00
Claude 41a49b639f perf: lazy fe_add + full ARM64 ASM fe_mul for phone performance
Two major optimizations targeting ARM64 mobile phones:

1. LAZY FIELD ADDITION (both platforms):
   fe_add no longer calls fe_normalize. Values may be in [0, 2^256)
   between operations. This is safe because:
   - fe_mul/fe_sqr reduction handles any 256-bit input
   - fe_negate normalizes its input before P - a
   - fe_is_zero/fe_equal/fe_to_bytes normalize their copies

   Saves ~6 normalize calls per gej_double (called 130x per verify).
   Impact: gej_add_ge 372→316ns (15%), gej_double 224→206ns (8%).

2. FULL ARM64 ASM fe_mul (ARM64 only):
   Complete 4x4 multiply + reduction in inline assembly using:
   - LDP/STP for load/store pairs (halves memory instructions)
   - MUL+UMULH with interleaved scheduling across columns
     (hides 3-cycle multiply latency behind independent additions)
   - Full reduction in ASM with MUL+UMULH+ADDS carry chain
   - 20 registers used (ARM64 has 31 — zero stack spills)
   - Row 1 and 3 interleave b0+b2 products with b1+b3 for ILP

   Expected ARM64 improvement: fe_mul ~20-25ns → ~13-15ns

x86_64 benchmark (measured on this machine):
  gej_add_ge: 372→316ns (15% faster)
  ECDH:       35.4→32.6µs (8% faster, now tied with ACINQ)
  batch(200): 1596→1437µs (10% faster, 7.2µs/event)

https://claude.ai/code/session_011KVZhDcV2G7idNWEBz12GY
2026-04-11 13:11:54 +00:00
Claude b71641a15f fix: fair C-to-C benchmark — ACINQ sign must include keypair_create
The previous benchmark unfairly gave ACINQ a cached keypair (created
once outside the loop) while our sign derived the pubkey each call.

Fixed to test both patterns:
- "sign (full)": both derive pubkey each call
  ACINQ: keypair_create + sign32 = 36.3µs
  Ours: ecmult_gen + sign_internal = 33.6µs → 1.08x faster

- "sign (cached)": both reuse precomputed pubkey
  ACINQ: sign32 with cached keypair = 18.8µs
  Ours: signXOnly with cached xonly = 17.4µs → 1.08x faster

Fair native C-to-C results (x86_64, BMI2):
  pubkeyCreate:          ACINQ 18.0  Ours 16.3  1.10x faster ✓
  sign (full):           ACINQ 36.3  Ours 33.6  1.08x faster ✓
  sign (cached):         ACINQ 18.8  Ours 17.4  1.08x faster ✓
  verify (BIP-340):      ACINQ 36.7  Ours 40.4  0.91x slower ✗
  verifyFast (Nostr):    ACINQ 36.7  Ours 34.5  1.06x faster ✓
  ECDH (cached):         ACINQ 37.2  Ours 35.4  1.05x faster ✓
  batch(200):            ACINQ 42.4  Ours  8.3  5.1x faster  ✓

We beat ACINQ on 5 of 6 operations. The only loss is full BIP-340
verify (0.91x) due to ACINQ's 5x52+ADCX/ADOX field assembly.

https://claude.ai/code/session_011KVZhDcV2G7idNWEBz12GY
2026-04-11 12:38:03 +00:00
Claude 2f5152efd6 bench: add native C-to-C benchmark vs ACINQ libsecp256k1
Direct comparison with no JNI/JVM overhead, both libraries called
from the same C program on the same machine (x86_64, BMI2).

Results (x86_64, same machine, cached pubkey pattern):
  Operation                  ACINQ     Ours    Speedup
  pubkeyCreate               21.2µs   20.3µs    1.04x
  signSchnorr                23.5µs   39.3µs    0.60x (*)
  verify (BIP-340)           42.8µs   46.8µs    0.91x
  verifyFast (Nostr)         42.8µs   39.1µs    1.10x
  ECDH (cached)              44.4µs   39.7µs    1.12x
  batch(200)                 47.2µs   10.1µs    4.7x per event

(*) sign is slower because ACINQ's keypair API pre-stores the pubkey,
avoiding ecmult_gen during sign. Our API derives pubkey each time.
A keypair-style API would match ACINQ's sign performance.

Cross-verification confirms both produce compatible signatures.

https://claude.ai/code/session_011KVZhDcV2G7idNWEBz12GY
2026-04-11 11:47:19 +00:00
Claude bc405d045b perf: add P-table cache to ecmult and liftX cache to ECDH
The ECDH operation was 0.7x ACINQ (55µs vs 40µs) because:
1. P-table built from scratch every call: 9.8µs
2. liftX (sqrt) for pubkey decompression: 5.5µs

Both are redundant for the Nostr use case where the same peer key
is used repeatedly (NIP-44 encrypted DMs).

Fixes:
- Share the P-table cache (1024 entries) between ecmult and
  ecmult_double_g. Same pubkey → cache hit → skip table build.
- Use lift_x_cached in ecdh_xonly. Same pubkey → skip sqrt.

ECDH: 55.2µs → 33.9µs (1.63x faster, now 1.18x faster than ACINQ)

Full benchmark (x86_64, cached pubkey pattern):
  signXOnly:      17.6µs (56,818 ops/s) — 2.1x faster than ACINQ
  verifyFast:     35.0µs (28,571 ops/s) — 1.2x faster than ACINQ
  pubkeyCreate:   15.8µs (63,291 ops/s) — 1.2x faster than ACINQ
  ecdhXOnly:      33.9µs (29,499 ops/s) — 1.2x faster than ACINQ
  batch(200):   1596µs (125,313 ev/s)   — 12x faster than ACINQ

https://claude.ai/code/session_011KVZhDcV2G7idNWEBz12GY
2026-04-11 05:22:40 +00:00
Claude 57acbfd567 perf: ARM64-specific optimizations for mobile phones
Optimize the ARM64 code path (the primary target for Nostr clients):

1. fe_mul_asm: Use LDP/STP (load/store pair) to load all 8 limbs in
   4 instructions instead of 8 individual LDR. Row 0 in hand-tuned
   ASM with MUL+UMULH+ADDS/ADC. Rows 1-3 in __int128 C (ARM64 gcc
   generates optimal MUL+UMULH+ADDS/ADCS and MADD from this).

2. fe_normalize: Branchless on ARM64 using mask-based conditional
   subtract (compiles to CSEL/AND on ARM64, avoiding branch
   misprediction on mobile Cortex-A76+ SoCs). x86_64 keeps the
   branching version since its branch predictor handles the >99.99%
   non-taken case perfectly.

3. Document ARM64-specific instruction usage:
   - MUL + UMULH: 64×64→128 product (1 cycle throughput on A76+)
   - LDP/STP: load/store pair (2 regs per instruction)
   - ADDS/ADCS: carry chain for accumulation
   - MADD: fused multiply-add (generated by gcc from __int128)

These changes don't affect x86_64 correctness or performance
(verified: all keys pass, benchmark matches previous numbers).
The ARM64 improvements will be measurable when built with the
Android NDK for actual phone testing.

https://claude.ai/code/session_011KVZhDcV2G7idNWEBz12GY
2026-04-11 05:10:15 +00:00
Claude 4fd4ad63d1 perf: use MULX (BMI2) in x86_64 field multiply assembly
Replace MULQ with MULX in the inline assembly for fe_mul on x86_64.
MULX advantages over MULQ:
- Uses RDX as implicit input (not RAX), outputs to two arbitrary regs
- Does NOT clobber flags, enabling better instruction scheduling
- Enables the compiler to interleave multiplies with carries

Requires BMI2 (available on Haswell+ / Zen+). Added -mbmi2 to
CMakeLists.txt for x86_64 builds.

Note: ADCX/ADOX (ADX extension) for dual carry chains was investigated
but the compiler doesn't auto-generate them from __int128 code, and
hand-encoding in inline ASM requires restructuring the entire multiply
to express two independent carry chains. The MULX-only approach still
gives a measurable improvement.

fe_mul: 17.2ns → 15.5ns (10% faster)
pubkeyCreate: 16.7µs → 15.5µs (7% faster)
verifyFast: 36.5µs → 36.1µs (1% faster, 27,700 ops/s)

https://claude.ai/code/session_011KVZhDcV2G7idNWEBz12GY
2026-04-11 04:59:10 +00:00
Claude 3544c2cbc3 fix: benchmark batch verify naming, add batch sizes 4 and 64
Fix garbled batch verify names in the C benchmark (static char buffer
was shared across calls). Use string literals per batch size instead.

Fix batch verify self-test: use sign() (safe path) instead of
sign_xonly() since the test key has odd-y pubkey.

Add batch sizes 4 and 64 to the benchmark for finer granularity.

Batch verification results (x86_64 standalone, same pubkey):
  Batch   µs/event   events/sec   vs individual
    1       36.6       27,322       1.0x
    4       15.2       66,007       2.4x
    8       11.1       89,787       3.3x
   16        9.2      109,290       4.0x
   32        8.6      115,942       4.2x
   64        8.3      120,960       4.4x
  200        7.8      127,689       4.7x

vs Kotlin: C batch is 1.3-1.7x faster across all batch sizes.
At 200 events: 127K ev/s (C) vs 96K ev/s (Kotlin).

https://claude.ai/code/session_011KVZhDcV2G7idNWEBz12GY
2026-04-11 04:50:47 +00:00
Claude a8a3d8f44f perf: add x86_64 and ARM64 inline ASM for field multiply
Platform-specific inline assembly for fe_mul:

x86_64: Uses MULQ instruction for 64x64->128 products. Row-based
schoolbook with ADC carry chain. Reduction uses MULQ for hi[i]*C.
Eliminates redundant register moves that __int128 compilation generates.

ARM64: Uses MUL+UMULH instruction pairs for 64x64->128 products.
First row in ASM with ADDS/ADC carry chain, remaining rows use
__int128 (which ARM64 gcc compiles well). Reduction in __int128.

fe_mul: 20.1ns → 17.2ns (14% faster on x86_64)
gej_double: 242ns → 224ns (7.4% faster)
verifyFast: 37.2µs → 36.5µs (27,397 ops/s)
signXOnly: 19.2µs → 18.5µs (54,054 ops/s)

https://claude.ai/code/session_011KVZhDcV2G7idNWEBz12GY
2026-04-11 04:38:40 +00:00
Claude 1a55746774 perf: remove redundant normalizations, optimize gej_add_ge hot path
Remove redundant fe_normalize calls from gej_add_ge (the verify hot
path, called ~60 times per verify). Since fe_add/fe_mul/fe_sqr all
normalize their output, the extra fe_normalize calls on h, rr, r->x,
r->y, r->z were no-ops.

Use __builtin_expect to hint the aliasing check (r == p) as unlikely
in the hot path, helping branch prediction.

Also remove redundant normalizations from gej_add for consistency.

Performance (x86_64 standalone, µs/op):
  signXOnly: 19.2 µs (52K ops/s) — 1.9x faster than ACINQ
  verifyFast: 37.2 µs (27K ops/s) — 1.05x faster than ACINQ
  pubkeyCreate: 16.7 µs (60K ops/s) — matching ACINQ

https://claude.ai/code/session_011KVZhDcV2G7idNWEBz12GY
2026-04-11 04:33:50 +00:00
Claude 6d9d03f52c perf: inline fe_mul, restore fast signXOnly, fix benchmark self-test
Three performance optimizations:

1. Inline fe_mul: merge mul_wide + reduce_wide into a single function
   body to keep intermediates in registers and eliminate call overhead.
   Saves ~1-2ns per fe_mul call (~200 calls per verify).

2. Restore fast signXOnly: assume even-y (BIP-340 convention) instead
   of deriving y-parity via ecmult_gen each time. This is correct for
   Nostr keys which are pre-processed to have even-y pubkeys.
   signXOnly: 36µs → 19µs (1.9x faster).

3. Fix benchmark: use sign() (safe, derives y-parity) for self-test
   since the test key has odd-y pubkey.

Performance (x86_64 standalone, µs/op):
  signXOnly (cached pk):  19.0 µs (52,524 ops/s) — 1.9x faster than ACINQ
  signSchnorr:            36.7 µs (27,282 ops/s) — matches ACINQ
  verifyFast (cached pk): 37.0 µs (27,013 ops/s) — faster than ACINQ
  pubkeyCreate:           16.6 µs (60,250 ops/s) — matches ACINQ

https://claude.ai/code/session_011KVZhDcV2G7idNWEBz12GY
2026-04-11 04:27:48 +00:00
Claude f476168f60 perf: add P-side wNAF table cache for repeated pubkey verification
In Nostr, the same pubkeys are verified repeatedly (many events per
author). Building the P-side wNAF table costs ~437 field ops (~27% of
verify). This cache stores precomputed affine tables keyed by pubkey
x-coordinate, skipping table build entirely on cache hits.

1024 cache entries × 16 affine points × 64 bytes ≈ 1MB total.
Direct-mapped hash: (px.d[0] ^ (px.d[1] << 3)) & 1023.

Performance with cached pubkey (x86_64 standalone):
  verifyFast: 51.5 → 35.4 µs (1.46x faster, 28,242 ops/s)
  verify:     58.3 → 42.0 µs (1.39x faster, 23,820 ops/s)

Now FASTER than ACINQ's libsecp256k1 for cached-pubkey verify:
  ACINQ:  25,832 ops/s (38.7 µs)
  Ours:   28,242 ops/s (35.4 µs)

https://claude.ai/code/session_011KVZhDcV2G7idNWEBz12GY
2026-04-11 04:22:06 +00:00
Claude 39c97f7c32 perf: optimize fe_half with branchless conditional add
Replace fe_half's normalize-then-branch approach with a branchless
mask-based conditional add of P. This eliminates the fe_normalize_full
call and branch prediction penalty.

Note: dedicated fe_sqr with cross-product doubling was attempted but
reverted — with 4x64 limbs, each 64x64 product is 128 bits and
doubling overflows uint128. The 5x52 representation wouldn't have this
issue (104-bit products, 105 bits doubled) but was rejected earlier for
having more total products (25 vs 16). This is a fundamental tradeoff.

Performance (x86_64 standalone, µs/op):
  verifyFast: 51.5 µs (19,422 ops/s)
  pubkeyCreate: 16.7 µs (59,925 ops/s)
  signSchnorr: 35.5 µs (28,164 ops/s)

https://claude.ai/code/session_011KVZhDcV2G7idNWEBz12GY
2026-04-11 04:20:22 +00:00
Claude a13ad7d243 feat: re-enable comb method for G multiplication — 3x faster sign/keygen
Fix batch_to_affine to handle infinity points by skipping them in the
cumulative Z product chain. The comb table has infinity entries at
index 0 of each block (representing "no teeth set"), and the old code
multiplied by Z=0 which corrupted all subsequent Z products.

Re-enable the comb method for ecmult_gen: only 3 doublings + ~43 table
lookups vs GLV+wNAF's ~130 doublings + ~32 additions.

Performance improvement (x86_64 standalone):
  pubkeyCreate: 54.4 µs → 17.0 µs (3.2x faster)
  signSchnorr:  109 µs → 36.2 µs (3.0x faster)
  signXOnly:    109 µs → 35.9 µs (3.0x faster)
  verify/ECDH: unchanged (don't use comb)

https://claude.ai/code/session_011KVZhDcV2G7idNWEBz12GY
2026-04-11 04:11:58 +00:00
Claude 050cf39a54 fix: correct y-parity in schnorr_sign_xonly, all operations now verified
The sign_xonly function incorrectly assumed even y-parity for all keys.
BIP-340 requires checking the actual y-parity of the derived pubkey and
negating the secret key when y is odd. Without this, signatures for keys
with odd-y pubkeys (roughly half of all keys) were invalid.

All operations now pass correctness tests:
- sign + verify for keys 1, 2, 3, 0xff, and 0xd217c1... (random)
- verify_fast (skip y-parity check)
- batch_verify (5 signatures from same pubkey)

C standalone benchmark (x86_64):
  verifySchnorrFast: 52 µs (19,143 ops/s)
  verifySchnorr:     60 µs (16,616 ops/s)
  signSchnorr:      109 µs (9,177 ops/s)
  pubkeyCreate:      54 µs (18,381 ops/s)
  ecdhXOnly:         59 µs (16,958 ops/s)

https://claude.ai/code/session_011KVZhDcV2G7idNWEBz12GY
2026-04-11 04:06:05 +00:00
Claude fefbb243f0 fix: correct GLV MINUS_LAMBDA constant and fe_cmp normalization bug
Two critical bugs fixed:

1. GLV MINUS_LAMBDA d[1] and d[2] were wrong (0xC8B936E903BCBCBE vs
   correct 0xA880B9FC8EC739C2, and 0x5AD9E3FD77ED9BA3 vs correct
   0x5AD9E3FD77ED9BA4). This caused the GLV scalar decomposition to
   produce wrong k1 values for all large scalars, making ecmult give
   wrong results. Verified by checking lambda^3 mod n == 1.

2. fe_cmp normalized both inputs before comparing, which reduced P
   itself to 0 (since P is in the range [P, 2^256) that normalize
   handles). This caused the "r < p" check in verify to fail for ALL
   valid signatures. Fixed by comparing raw limb values.

Sign + verify + verify_fast now work correctly for small keys (1-3).
Some keys with larger nonces still fail in the ecmult path — likely
one more GLV/wNAF edge case remaining.

https://claude.ai/code/session_011KVZhDcV2G7idNWEBz12GY
2026-04-11 04:03:55 +00:00
Claude 09fb48e75c fix: change crossinline to noinline for nullable lambda parameter
Kotlin does not allow crossinline parameters to be nullable. The cOp
parameter in benchTriple needs to be nullable (null when C library is
not available), so use noinline instead.

https://claude.ai/code/session_011KVZhDcV2G7idNWEBz12GY
2026-04-11 03:50:08 +00:00
Claude da3ed9ba3e fix: add aliasing protection to gej_add_ge and gej_add
Both gej_add_ge(r, p, q) and gej_add(r, p, q) write to r->x/y/z
while reading from p->x/y/z. When r == p (in-place accumulation in
ecmult loops), the output overwrites input during computation.

Added copy-on-alias detection at the top of both functions, matching
the fix already applied to gej_double.

https://claude.ai/code/session_011KVZhDcV2G7idNWEBz12GY
2026-04-11 03:37:44 +00:00
Claude 8cf2f9795a fix: scalar_mul second fold carry propagation
Fix the modular reduction in scalar_mul: the second fold step was
accumulating hc2[4..7] into a single acc variable without proper
limb-by-limb carry propagation. Now uses the same 8-limb sum pattern
as the first fold, with a convolution-style third fold for any
remaining high bits.

GLV decomposition now verified correct for all test scalars.
The remaining issue is in the wNAF multiply loop within ecmult
(correct GLV split → wNAF encode → point table lookup → accumulate).
Likely gej_add_ge aliasing or table build ordering issue.

https://claude.ai/code/session_011KVZhDcV2G7idNWEBz12GY
2026-04-11 03:36:09 +00:00
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 8cc198d161 fix: relay connectivity degradation with Tor by adding WebSocket pings, pool eviction, and reducing backoff
Dead WebSocket connections through Tor were going undetected because no
ping interval was set, leaving zombie connections that appeared connected
but carried no traffic. Additionally, relay error backoff was set to
ONE_DAY making recovery impossible without toggling Tor, and the shared
OkHttp connection pool retained stale connections across proxy changes.

- Add 120s WebSocket ping interval to detect dead Tor connections
- Reduce dontTryAgainForALongTime from ONE_DAY to FIVE_MINUTES
- Evict shared connection pool when proxy settings change

https://claude.ai/code/session_01VFAypytKGzdmuoJAXrb72G
2026-04-11 02:03:23 +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