In mulDoubleG (verify), the P-side wNAF-5 affine table (8 odd multiples
of P plus 8 GLV λ counterparts) is rebuilt from scratch on every call.
This costs ~437 field ops: 1 doublePoint + 7 addPoints + 8 β-multiplies
+ 1 batch inversion with full field inversion (~270 ops alone).
Add a 256-entry direct-mapped cache that stores these affine tables
keyed by the point's x-coordinate. On cache hit, the table build is
skipped entirely — the cached arrays are used directly (zero-copy).
This saves ~27% of mulDoubleG cost (~20% of total verify) for repeated
pubkeys. Combined with the liftX cache from the previous commit,
repeated-pubkey verifications now skip ~33% of the work.
JVM benchmark: verify ratio improved from 2.0x to 1.6-1.8x vs native C
(with the benchmark's 100% cache hit rate). Original baseline was 3.0x.
https://claude.ai/code/session_017UbWduFi1sLUsgVUMUH2nx
Add in-place ScalarN operations (mulTo, addTo, negTo) that write to
caller-provided output arrays instead of allocating. Add
Glv.splitScalarInto that uses pre-allocated scratch buffers from
PointScratch, eliminating ~26 LongArray allocations per call (called
2x per verify = ~52 allocs saved). Both mul and mulDoubleG now use
the allocation-free split path.
Pool all LongArray and MutablePoint allocations in verifySchnorr
into thread-local PointScratch (verifyPx/Py/R/S/E/Rx/Ry, verifyPPoint,
verifyResult). Add U256.fromBytesInto for decoding into pre-allocated
arrays. Total: ~14 object allocations eliminated per verify call.
Add pubkey decompression cache (256-entry direct-mapped) that skips
the liftX square root (~280 field ops) for repeated pubkeys. In Nostr,
the same authors are verified repeatedly, so cache hit rate is high.
This saves ~13% of verify cost per cache hit.
Increase benchmark warmup/iterations for more stable measurements
(e.g., verifySchnorr: 200/500 -> 2000/5000).
JVM benchmark result: verify ratio improved from 3.0x to 2.0x vs
native C (with 100% cache hit rate on the benchmark's single pubkey).
https://claude.ai/code/session_017UbWduFi1sLUsgVUMUH2nx
Add route-based screens for group management instead of dialogs:
- RemoveMemberScreen: lists removable members with confirmation dialog
- EditGroupInfoScreen: edits group name and description via MLS metadata
- Wire Account/ViewModel methods for removeMember and updateGroupMetadata
- Add Edit and Remove Member actions to MarmotGroupInfoScreen toolbar
- Fix pre-existing exhaustive when branch for GroupEventResult.Duplicate
https://claude.ai/code/session_01LhfCp8DHqNVx6mSiYfpQny
The previous counts (200 warmup / 500 iterations for verify) were too
low for reliable measurements on noisy VMs. Increase to 2000+ warmup
and 3000-50000 iterations depending on operation cost, giving the JIT
time to fully optimize and reducing variance between runs.
https://claude.ai/code/session_017UbWduFi1sLUsgVUMUH2nx
Unroll hot-path loops in U256 (mulWide, sqrWide, addTo, subTo) to
eliminate loop control overhead and array bounds checks that ART JIT
on Android does not optimize as aggressively as HotSpot. These functions
are called ~1,900× per signature verification.
Optimize unsignedMultiplyHighFallback to compute the unsigned 128-bit
product directly from 32-bit sub-products, avoiding the signed
multiplyHigh + correction path. Saves ~8 instructions per call on
Android < API 31 (~30,000 calls per verify).
Unroll FieldP.reduceWide carry propagation and inline the reduceSelf
subtraction (avoiding P array access and U256.subTo call). Unroll
FieldP.half with P[1..3]=-1 inlined as mask.
Replace copyFrom pattern in mul, mulG, and mulDoubleG with ping-pong
point buffers — alternate between two MutablePoints via reference swap
instead of copying 12 Longs after every addition. Eliminates ~170
copyFrom calls per verify and avoids the internal copy buffer in
doublePoint's out===inp path (~130 per verify). Also removes the
MutablePoint() allocation in mulG (3 LongArrays per sign/pubCreate).
https://claude.ai/code/session_017UbWduFi1sLUsgVUMUH2nx
All three annotated methods (toggleAudioMute, toggleVideo, cleanup)
are only ever called from the main thread, so the blocking lock was
never contended. Remove it to avoid misleading future readers and to
prevent accidental main-thread blocking if a call site changes.
The Mutex in CallManager is kept because it uses suspending withLock
which never blocks the thread, and it correctly serializes the
GlobalScope.launch(Dispatchers.Default) hangup/reject paths in
CallActivity against the viewModelScope state-collector path.
https://claude.ai/code/session_01TTPrYjcz1eYEzdSV5N6rHi
- Clear discoveredCalleePeers on transitionToEnded to prevent stale
peers from triggering mesh setup in subsequent calls
- Cap processedEventIds and completedCallIds with LRU eviction to
prevent unbounded memory growth over long app sessions
- Store and release SurfaceTextureHelper in startCamera/stopCamera
to prevent GL thread and texture leaks
- Wrap stopCamera's stopCapture in try-catch for InterruptedException
to ensure camera resources are always released
- Add Mutex to CallManager to serialize state mutations, preventing
races when signaling events arrive concurrently from multiple relays
- Add @Synchronized to CallController's toggleAudioMute, toggleVideo,
and cleanup to prevent races with WebRTC callbacks
https://claude.ai/code/session_01TTPrYjcz1eYEzdSV5N6rHi
- H11: Cache skipped message keys in SecretTree for out-of-order decryption
- L1: Prune sentKeys map when exceeding 10000 entries
- L2: Prune consumed generations below current minimum
- H17: Call markAsRead when chat view is opened
- Key zeroing: Zero old private keys in KeyPackageRotationManager
https://claude.ai/code/session_018gVkmmYgMFtBH7G31pCk9N
ThreadLocal.withInitial is a JVM-only API that doesn't compile on
Kotlin/Native or iOS targets. Replace all usages in commonMain
(FieldP.kt, Point.kt) with a new ScratchLocal expect/actual:
- jvmMain/androidMain: delegates to java.lang.ThreadLocal (same behavior)
- nativeMain: holds value directly (Kotlin/Native coroutines are
cooperative, scratch buffers don't need thread isolation)
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
When the app restarts and reconnects to relays, old call offer events
from completed calls could be replayed, causing the phone to ring for
calls that already ended. This was especially noticeable when the user
killed and restarted the app shortly after a call.
Three protections added to CallManager:
1. Track completed call IDs: hangup/reject events mark their call-id as
completed. Subsequent offer events for the same call-id are ignored.
2. Init timestamp guard: events created before the CallManager was
initialized (minus a grace period) are rejected, preventing stale
events from a previous app session from triggering ringing.
3. Completed call IDs survive reset(): the set is intentionally not
cleared when the call state machine resets to Idle, ensuring that
stale offers remain blocked for the lifetime of the CallManager.
https://claude.ai/code/session_0145VKiG8yZMqcMsaBEjNPxv
Three Android-specific optimizations:
1. Use Math.unsignedMultiplyHigh on API 35+ (Android 15): single UMULH
instruction, eliminates the 4-insn signed→unsigned correction that
the fallback path requires. Same optimization as our JVM 18+ path.
2. Use Math.multiplyHigh + correction on API 31-34 (Android 12-14):
avoids the pure-Kotlin 4×32-bit sub-product fallback entirely.
3. Resolve API level check ONCE at class load via static final fields
(HAS_MULTIPLY_HIGH, HAS_UNSIGNED_MULTIPLY_HIGH) instead of checking
Build.VERSION.SDK_INT on every call. These functions are called 16×
per field multiply (~12,000× per signature verify), so eliminating
the per-call branch matters.
Performance tiers on Android:
API 35+ (Android 15): ~same as JVM 18+ (UMULH intrinsic)
API 31-34 (Android 12-14): SMULH + 3 correction insns per product
API 26-30 (Android 8-11): pure-Kotlin fallback (4 sub-products)
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
Add precise per-operation cost analysis from comparing our Kotlin
implementation with bitcoin-core/secp256k1's C source:
- doublePoint: 1,516 insns (Kotlin) vs 530 insns (C) = 2.9×
- mul/sqr accounts for 76% of gap (UMULH+MUL+carry vs single MUL)
- add/neg/half accounts for 24% (reduceSelf vs lazy magnitude tracking)
- Update performance numbers to Java 21 results (verify 3.4×, sign 1.1×)
- Document all optimizations implemented during this session
- Note that lazy reduction penalty (4.2× on cheap ops) is the main
remaining algorithmic opportunity, but requires 5×52-bit limb change
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
- Add `inline` to hot-path tiny functions: U256.isZero, U256.testBit,
FieldP.reduceSelf, FieldP.neg, MutablePoint.isInfinity. These are
called thousands of times per EC operation; inline eliminates virtual
call overhead (mostly helps Kotlin/Native; JVM JIT already inlines).
- Fix stale comment: G_TABLE_SIZE is 1024 for w=12 (was "64 for w=8")
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
Move transitionToEnded() before the signing + relay publish in hangup()
so the UI stops ringing/ringback immediately, matching the pattern
already used by rejectCall(). Add onDestroy safety net in CallActivity
to hang up if the Activity is destroyed while a call is active. Wrap
audio stop methods in try-catch to prevent one failure from blocking
the others.
https://claude.ai/code/session_01Rip2HPCbF48PPFDiB2X5ik
inv() and sqrt() each allocated 11 LongArray(4) (plus 2 for sqrt
verification) on every call. These are now served from a thread-local
Array(11) { LongArray(4) } cache, eliminating 22-24 allocations per
ECDH operation (inv called in toAffine, sqrt called in liftX).
Also reuses chain scratch slots for sqrt's verification step instead
of allocating separate check/ar arrays.
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg