Creates Secp256k1Benchmark.kt in the benchmark module using AndroidX
Benchmark (Jetpack Microbenchmark) to measure all secp256k1 operations
on real Android hardware/emulator with proper warmup and statistics.
Benchmarks: verifySchnorr, signSchnorr, compressedPubKeyFor,
secKeyVerify, pubKeyCompress, privateKeyAdd, pubKeyTweakMulCompact.
Run with: ./gradlew :benchmark:connectedAndroidTest
The existing SignVerifyBenchmark already tests sign/verify through
Nip01Crypto, but this new benchmark tests the Secp256k1Instance
operations directly, matching the JVM benchmark for comparison.
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
Compares the 32-byte key against n byte-by-byte (big-endian) instead of
decoding into limbs first. Eliminates one IntArray(8) allocation and the
fromBytes conversion. The non-zero check ORs all bytes in a single pass.
privKeyTweakAdd was also tested with a byte-based approach but reverted
because 32 byte-iterations are slower than 8 limb-iterations — the JVM
optimizes Long operations better than byte loops.
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
For uncompressed keys (04 || x || y), compression only needs the y parity
bit and a copy of x. The previous implementation decoded both coordinates
into field limbs, validated y²=x³+7 with 2 field muls, then re-encoded.
The new implementation reads the last byte of y (parity bit), copies the
32 x-bytes, and sets the 02/03 prefix. No IntArray allocations, no field
arithmetic, no curve validation — just byte manipulation.
For already-compressed input, returns the input unchanged.
Benchmark: pubKeyCompress 658K → 6.7M ops/s (was 4.5x slower, now 2.4x faster than native)
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
Fills coverage gaps identified by audit, especially for areas where
bugs were found during development:
GlvTest (4 → 14 tests):
- wNAF reconstruction for small, large, and high-bit scalars
- wNAF carry overflow at bit 255 (regression test for the fixed bug)
- wNAF digits are odd and bounded, zero-run guarantee verified
- splitScalar with zero, n-1, and 5 different scalar values
- splitScalar halves are ~128 bits (upper limbs zero)
- β³ ≡ 1 (mod p) verification
- mulDoubleG with zero e scalar
FieldPTest (22 → 27 tests):
- half(p-1), inv(2), sqrt(0), sqrt(1)
- mul aliasing (output == input)
PointTest (22 → 25 tests):
- addMixed with equal points (should double)
- addMixed with inverse points (should give infinity)
- parsePublicKey with compressed odd-y key round-trip
Secp256k1Test (14 → 17 tests):
- verifySchnorr with wrong message (negative test)
- verifySchnorr with corrupted signature (negative test)
- signSchnorr deterministic (null auxrand produces same signature)
Total: 126 → 146 tests
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
Splits the 914-line Point.kt into two focused files:
- Glv.kt (248 lines): GLV endomorphism constants, scalar decomposition
(splitScalar), Babai rounding (mulShift384), and wNAF encoding. This is
a self-contained algorithm that only operates on scalars (no EC points).
- Point.kt (683 lines): EC point types, core operations (double, addMixed,
addPoints), scalar multiplication (mul, mulG, mulDoubleG), coordinate
conversion (toAffine, liftX), and key serialization.
Each file has a comprehensive header explaining its purpose and the
algorithms it implements. The Point.kt header is updated to reflect the
current state (GLV and wNAF are implemented, not "future optimizations").
mulDoubleG now references Glv.splitScalar and Glv.wnaf instead of local
methods. GlvTest updated to use the Glv object directly.
No functional changes — pure file reorganization with updated documentation.
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
- Replace 3-way ByteArray concatenation for tagged hash inputs with
single pre-sized array + copyInto calls (avoids 3 intermediate arrays)
- Add U256.fromBytes(bytes, offset) overload to decode from a slice
without copyOfRange allocation
- Build signature output using toBytesInto instead of concatenation
- Apply same pattern to signSchnorr's nonce and challenge hash inputs
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
Karatsuba multiplication (splitting 8 limbs into 4+4 halves for 48 inner
products instead of 64) was implemented and tested but reverted because
the overhead of extra additions, carry propagation, and 5 temporary array
allocations per call negates the product-count savings at only 8 limbs.
The crossover point where Karatsuba beats schoolbook is typically ~32+
limbs on hardware with fast multiply.
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
Three targeted optimizations in the verify hot path:
1. Precompute and cache lambda(G) table: the 8 AffinePoints for λ(G)
odd-multiples are now lazily initialized once (like gTable) instead
of recomputing 8 field multiplications per verify call.
2. Efficient P odd-multiples table: build [1P, 3P, 5P, ..., 15P] via
1 doubling + 7 additions (compute 2P then add repeatedly) instead
of building all 16 multiples [1P..16P] with 15 additions and
discarding the even ones.
3. Pre-allocated Jacobian negation scratch: the MutablePoint used for
negating P-side table entries (when wNAF digit is negative) is now
allocated once before the main loop instead of per-digit.
Also removed the unused maybeNegateTable function.
Benchmark: verifySchnorr 3,429 → 3,556 ops/s (7.2x vs native)
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
Replaces generic square-and-multiply exponentiation in inv() and sqrt()
with hand-crafted addition chains derived from libsecp256k1.
The key insight: p-2 and (p+1)/4 have long runs of 1-bits (since p ≈ 2^256),
so generic powModP wastes ~230+ multiplications on redundant mul-by-base steps.
The addition chain instead builds a^(2^k - 1) for k = 2,3,6,9,11,22,44,88,176,
220,223 via a ladder of squarings, then combines them with a short tail.
Savings per call:
inv: 255 sqr + 15 mul = 270 ops (was 255 sqr + 248 mul = 503 ops) → -233 muls
sqrt: 254 sqr + 13 mul = 267 ops (was 253 sqr + 246 mul = 499 ops) → -233 muls
Each verify does one inv (toAffine) + one sqrt (liftX), saving ~466 field
multiplications = ~29,800 fewer inner products per verification.
Also removes the now-unused generic powModP function and its P_MINUS_2 /
P_PLUS_1_DIV_4 exponent constants.
Benchmark: verifySchnorr 3,254 → 3,429 ops/s (~5% faster, 8.2x vs native)
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
Implements the secp256k1 GLV (Gallant-Lambert-Vanstone) endomorphism to
halve the number of point doublings during signature verification.
How it works: secp256k1 has an efficiently computable endomorphism
φ(x,y) = (β·x, y) where β is a cube root of unity in the field.
The corresponding scalar λ satisfies λ·P = φ(P). Any 256-bit scalar k
can be decomposed into k = k₁ + k₂·λ (mod n) where k₁, k₂ are ~128 bits.
This means k·P = k₁·P + k₂·(β·P.x, P.y), requiring only ~130 doublings
instead of 256.
For verification (s·G - e·P), both scalars are split into halves,
giving 4 streams processed in a single pass: s₁·G, s₂·λ(G), e₁·P, e₂·λ(P).
Key fixes from earlier debugging:
- MINUS_LAMBDA constant was wrong (byte-level transcription error)
- G1/G2 Babai rounding constants were truncated to ~142 bits instead of
the full 256-bit values from libsecp256k1
- wNAF overflow fix: extended working array with maxOf(totalBits, scalar.size)
to handle scalars larger than maxBits (IntArray(8) > IntArray(5) for 129-bit)
- GLV sign handling: XOR the negation flag with each wNAF digit sign instead
of pre-baking into tables (avoids double-negation on negative digits)
- P-side uses Jacobian tables (avoids 8 expensive field inversions that
would negate the GLV speedup)
Tests: 4 new GLV-specific tests (scalar split reconstruction, endomorphism
correctness, wNAF+GLV k1*G, mulDoubleG with zero scalar)
Benchmark improvement for verifySchnorr:
Before (wNAF only): 2,626 ops/s (10.6x vs native)
After (wNAF + GLV): 3,254 ops/s (8.7x vs native)
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
Fixed the wNAF encoding bug and wired it into the verification hot path.
Bug fix: The wNAF encoding silently dropped carry bits that overflowed past
the 256-bit scalar boundary. When a negative digit near bit 251 caused a
carry to bit 256, the extraction window was clamped to 0 bits by
`w.coerceAtMost(maxBits - bit)`, causing the carry digit to be lost.
Fixed by extending the working copy and result arrays to accommodate
carries up to bit (maxBits + w), and using totalBits instead of maxBits
for the window size clamp.
Performance: wNAF-5 (windowed Non-Adjacent Form, width 5) encodes scalars
using signed odd digits {±1, ±3, ..., ±15} with guaranteed ≥4 zero-runs
between non-zero digits. This reduces point additions in mulDoubleG from
~120 (4-bit window) to ~86 for two 256-bit scalars, a ~28% reduction in
additions while the 256 doublings remain the same.
Benchmark improvement for verifySchnorr:
Before: 1,940 ops/s (12.9x vs native)
After: 2,626 ops/s (10.6x vs native)
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
Each object now lives in its own file for easier navigation and review:
- U256.kt (284 lines): Raw 256-bit unsigned integer arithmetic with the
file-level architecture documentation explaining representation choices
- FieldP.kt (360 lines): Field arithmetic modulo the secp256k1 prime p,
including reduction, inversion, and square root
- ScalarN.kt (230 lines): Scalar arithmetic modulo the group order n,
including wide reduction and Fermat inversion
No functional changes — pure file reorganization.
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
Major cleanup of the pure-Kotlin secp256k1 implementation for readability:
Documentation:
- Added file-level architecture comments explaining representation choices
(why 8×32-bit limbs, why not 5×52-bit like C), field reduction strategy,
and performance approach (mutable output params, thread-local scratch)
- Added single-paragraph explainers for domain jargon: Jacobian coordinates,
Fermat inversion vs safegcd, windowed scalar multiplication, Shamir's trick,
GLV endomorphism, wNAF encoding
- Documented every public function with purpose, cost, and usage context
- Added inline comments explaining the math in point doubling/addition formulas
Removed dead code (-400 lines):
- straussGlvGP: GLV-accelerated Strauss method (had sign-handling bug)
- scalarSplitLambda, SplitResult, isHigh: GLV scalar decomposition
- wnaf, getBitsVar, addBitTo: wNAF encoding functions
- mulLambdaAffine, addMixedWithSign, buildOddMultiplesTable: GLV support
- All GLV constants (BETA, LAMBDA, MINUS_LAMBDA, G1, G2, MINUS_B1, MINUS_B2)
- U256.mulShift: used only by GLV scalar decomposition
These are preserved in git history and can be restored once the wNAF
interaction bug with the verify path is understood and fixed.
Structure:
- Field.kt: Clear sections (U256 → FieldP → ScalarN) with headers
- Point.kt: Sections (types → doubling → mixed add → full add → scalar mul
→ conversion → serialization) with formula documentation
- Secp256k1.kt: Grouped by purpose (keys → BIP-340 → tweaks) with
algorithm steps documented in KDoc
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
Phase 1 optimizations from C code analysis:
1. Dedicated sqrWide: Exploits a[i]*a[j] symmetry — 36 inner products
instead of 64. Reduces field squaring cost by ~40%.
2. Mixed Jacobian+Affine addition (addMixed): 8M+3S instead of 12M+4S.
Saves 4 multiplications per addition when one operand is affine.
Used for precomputed G table lookups during Shamir's trick.
3. Optimized point doubling (3M+4S via fe_half): Uses the (3/2)*X²
formula from libsecp256k1, replacing a field multiplication with a
cheap halving operation (carry-propagating right shift).
4. fe_half: Branchless divide-by-2 mod p, used by the new doubling formula.
5. AffinePoint type: Stores precomputed table entries as (x,y) without z,
enabling mixed addition. G table now stored as affine.
6. U256.mulShift: 256x256→shift multiplication for future GLV scalar
decomposition.
7. GLV infrastructure (straussGlvGP, scalarSplitLambda, wNAF, endomorphism
constants): Implemented but not yet wired into the verify hot path due
to sign-handling bugs being debugged. The 4-stream Strauss with GLV
will halve doublings from 256→128 once the sign logic is fixed.
Benchmark: verifySchnorr 1,940 → 2,116 ops/s (~9% improvement)
The modest gain reflects that only G-side additions use mixed add;
P-side still uses full Jacobian. GLV will provide the next big jump.
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
Key optimizations:
1. Mutable field operations: All FieldP hot-path methods (add, sub, mul, sqr)
now write into caller-provided output arrays instead of allocating new ones.
Thread-local IntArray(16) scratch for mulWide avoids per-mul allocation.
2. Mutable point operations: MutablePoint replaces immutable JPoint. Point
doubling/addition write into output points. Aliasing protection via
thread-local copy buffer for in-place doublePoint(out, out).
3. 4-bit windowed scalar multiplication: Processes 4 bits per iteration
(16 table entries) instead of 1 bit. Reduces point additions by ~4x.
4. Precomputed G table: Static lazy table of 16*G multiples. Generator
multiplication (signing, key creation) uses precomputed table directly.
5. Shamir's trick (mulDoubleG): Computes s*G + e*P in a single pass for
verification, eliminating the need for two separate scalar multiplications.
This roughly halves the cost of verifySchnorr.
6. Cached BIP-340 tag hashes: SHA256("BIP0340/challenge") etc. computed
once and reused, eliminating 2 SHA256 calls per verify.
7. toBytesInto: Writes directly into existing ByteArray at offset,
avoiding intermediate allocations in serialization.
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
Implements all secp256k1 operations used by Secp256k1Instance in pure Kotlin,
eliminating the dependency on native secp256k1 bindings (fr.acinq.secp256k1-kmp).
Implementation:
- Field.kt: 256-bit unsigned integer arithmetic, field mod p, scalar mod n
- Point.kt: EC point operations (Jacobian coords), point parsing/serialization
- Secp256k1.kt: Public API - pubkeyCreate, pubKeyCompress, secKeyVerify,
signSchnorr/verifySchnorr (BIP-340), privKeyTweakAdd, pubKeyTweakMul
Tests (36 total):
- All 19 BIP-340 test vectors (signing vectors 0-3, 15-18; verify vectors 4-14)
- ACINQ test vectors for key creation, compression, secKeyVerify, privKeyTweakAdd,
pubKeyTweakMul
- ECDH symmetry and sign/verify round-trip tests
Secp256k1Instance is now a concrete object in commonMain instead of expect/actual,
delegating to the pure-Kotlin implementation. All existing NIP-44 and BIP-32 tests
pass with the new implementation.
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
Wire Marmot MLS group messaging into the app's Account lifecycle,
event processing pipeline, and relay subscription system.
New files:
- AndroidMlsGroupStateStore: encrypted file-based MLS state storage
using KeyStoreEncryption (AES/GCM backed by Android KeyStore)
- MarmotManager: central coordinator holding all Marmot components
(MlsGroupManager, KeyPackageRotationManager, subscription/inbound/
outbound processors, WelcomeSender)
- MarmotGroupEventsEoseManager: relay subscription manager for
kind:445 GroupEvent filters via AccountFilterAssembler
Account integration:
- MarmotManager initialized during Account startup with restoreAll()
- Outbound methods: sendMarmotGroupMessage, addMarmotGroupMember,
publishMarmotKeyPackage(s), createMarmotGroup
- GroupEvent (kind:445) and KeyPackageEvent (kind:30443) added to
LocalCache.justConsumeInnerInner() dispatch
Event routing:
- GroupEventHandler: processes inbound kind:445 events through
MarmotInboundProcessor, indexes decrypted inner events in LocalCache
- GiftWrapEventHandler extended: detects kind:444 WelcomeEvent after
NIP-59 unwrap, routes to MarmotManager.processWelcome(), triggers
KeyPackage rotation when needed
- WelcomeEvent enhanced with optional "h" tag for group ID routing
https://claude.ai/code/session_01W2LHazEt4E3W4hn8f7gWVW
Add four protocol-layer components for Marmot group messaging:
1. MarmotSubscriptionManager: Coordinates relay subscriptions for
GroupEvent (kind:445), GiftWrap (kind:1059), and KeyPackage
(kind:30443) events. Tracks per-group since timestamps for
pagination and syncs with MlsGroupManager state.
2. MarmotInboundProcessor: Processes incoming GroupEvents through
outer ChaCha20-Poly1305 decryption → MLS decrypt → inner event
extraction. Handles commit detection, conflict resolution via
CommitOrdering, and Welcome processing with KeyPackage rotation.
3. MarmotOutboundProcessor: Builds outbound GroupEvents by MLS
encrypting inner Nostr events, applying ChaCha20-Poly1305 outer
layer, and signing with ephemeral keys for sender privacy.
4. MarmotWelcomeSender: Wraps MLS Welcome messages through the
NIP-59 gift wrap pipeline for delivery to new group members.
All code in quartz/commonMain (protocol layer). Includes 25 tests
covering roundtrip encryption, subscription management, error
handling, ephemeral key usage, and Welcome wrapping.
https://claude.ai/code/session_01XC5umkmsFB7XQ7xdrouArt
- Removed redundant deleteOnExit() in test helper — explicit delete() after each test already handles cleanup; the shutdown hook was unnecessary overhead
Add missing test vector entries for scenarios that have tests but were
not listed in the spec:
- E10-E18: single callee detection, P2P flow sequence, group p-tags
for all event types, group member union, ICE serialization round-trip
- S22-S29: fresh events, wrong call-id handling, ICE forwarding,
peer-left callback, reset, call-type preservation, caller cancel
- R7: renegotiation preserves call-id
- B1-B12: new ICE Candidate Buffering section covering both global
and per-session buffer layers
- W1-W18: new Gift Wrap Round-Trip section covering NIP-44
encrypt/decrypt for all event types including group calls
- I6, I10: renegotiation answer, renumbered full P2P flow
https://claude.ai/code/session_01AfRYTRCvtKqqDxeKQujUrx
Replace implementation-specific class names (EphemeralGiftWrapEvent,
SealedRumorEvent) with protocol-neutral language so the spec reads as
a standalone NIP independent of any particular codebase.
https://claude.ai/code/session_01AfRYTRCvtKqqDxeKQujUrx
Add encrypted local storage for MLS group state so groups survive app
restarts, plus key lifecycle management:
- MlsGroupState: TLS-encoded serializable snapshot of complete group state
(group context, ratchet tree, epoch secrets, private keys, transcript hash)
- MlsGroupStateStore: interface for encrypted per-group storage (platform
implementations provide EncryptedSharedPreferences / encrypted file)
- MlsGroupManager: high-level coordinator managing group lifecycle, epoch
secret retention window (N-1 for late messages), state persistence,
and key export
- RetainedEpochSecrets: bounded window of past epoch decryption secrets
for out-of-order message handling
- MlsGroup.saveState()/restore(): roundtrip serialization via TLS codec
- MlsGroup.proposeSigningKeyRotation(): Update proposal with fresh Ed25519
signing key and X25519 encryption key for forward secrecy
- KeyPackageRotationManager: tracks consumed KeyPackages after Welcome
processing, handles slot rotation and proactive age-based rotation
- Tests: 16 tests covering state serialization roundtrips, manager lifecycle,
multi-group independence, and signing key rotation persistence
https://claude.ai/code/session_01MuRS2zSVm6A36HNFwG7M5p
- Split PeerSession.kt out of PeerSessionManager.kt (types, interface,
manager are now in separate files)
- Remove webRtcSessions duplication in CallController — PeerSessionManager
is now the single source of truth for session tracking; WebRtcCallSession
is retrieved via the adapter cast when WebRTC-specific APIs are needed
- Initialize PeerSessionManager eagerly with localPubKey (passed to
CallController constructor) instead of lazy suspend init — fixes early
ICE candidates being silently dropped before first suspend call
- Extract FakePeerSession into its own file for reuse across test files
- Remove assertion-only glare tiebreaker tests from NipACStateMachineTest
(now properly tested with real logic in PeerSessionManagerTest)
https://claude.ai/code/session_01AfRYTRCvtKqqDxeKQujUrx
Changes viewedPollResultNoteIds from Set<String> to Map<String, Long> where
the value is the expiration timestamp. Uses the poll's endsAt date if set and
in the future, otherwise now + 24 hours. Expired entries are pruned on each
new insertion. Serialized as JSON instead of StringSet.
https://claude.ai/code/session_01EkUYT4giQPUvbAJZ54o1se
- ThumbnailDiskCache: combine get()+decodeThumbnail() into single load()
method. Move bitmap resize logic into generateFromFile() so callers
just pass a source file and the cache handles decode+resize+save.
- ProfilePictureFetcher: remove unused `options` field, remove @Stable
annotation, delegate all resize logic to ThumbnailDiskCache.
- UserAvatar: fix stale doc comment.
https://claude.ai/code/session_01PBJS3HDrurLP3i5n4tMsCv
Resolve conflict in ZoomableContentView.kt: keep viewModelScope
from this branch (prevents cancellation on dialog dismiss) and
toast notification from main.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace profilepic:// string scheme with a ProfilePictureUrl data class.
Coil routes to the fetcher by type via Fetcher.Factory<ProfilePictureUrl>
instead of parsing a URI scheme string on every load. This avoids string
concatenation at the call site and string parsing in the fetcher/keyer.
https://claude.ai/code/session_01PBJS3HDrurLP3i5n4tMsCv
Extract the ICE candidate buffering, answer routing, and renegotiation
glare logic from CallController into a platform-independent
PeerSessionManager in commons/. This makes the three most critical
NIP-AC spec requirements testable as JVM unit tests with FakePeerSession:
1. Two-layer ICE candidate buffering (global + per-session)
- Global buffer: candidates arriving before any PeerConnection exists
- Per-session buffer: candidates arriving before setRemoteDescription
- Candidates buffered while ringing are preserved when accepting
2. Renegotiation glare handling (pubkey comparison tiebreaker)
- Higher pubkey wins, lower pubkey rolls back local offer
- Full glare scenario test with two PeerSessionManagers
3. Callee-to-callee mesh initiation (lower pubkey initiates)
CallController now delegates to PeerSessionManager via a PeerSession
interface, with WebRtcPeerSessionAdapter bridging to WebRtcCallSession.
https://claude.ai/code/session_01AfRYTRCvtKqqDxeKQujUrx
On cache miss, instead of reading the entire downloaded image into a
byte array (which could be 50MB+ for a malicious profile picture),
the fetcher now passes the NetworkFetcher result straight through to
Coil's normal streaming decoder pipeline — zero overhead vs current.
Thumbnail generation runs in background from Coil's disk cache file
using file-based BitmapFactory.decodeFile(), which is seekable
(supports two-pass bounds+decode) and never buffers the full file.
https://claude.ai/code/session_01PBJS3HDrurLP3i5n4tMsCv