- 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
- Atomic writes: write to temp file then rename, so readers never see
a partially written JPEG
- Deduplication: ConcurrentHashMap tracks in-flight URLs so multiple
composables requesting the same profile picture don't trigger
duplicate decode+save work
https://claude.ai/code/session_01PBJS3HDrurLP3i5n4tMsCv
Verify the full encrypt/decrypt pipeline for all 6 NIP-AC signaling event
types through Ephemeral Gift Wraps (kind 21059):
sign inner event → NIP-44 encrypt → gift wrap → unwrap → verify
Tests cover:
- Each event kind round-trips (offer, answer, ICE, hangup, reject, renegotiate)
- Third parties cannot decrypt wraps addressed to others
- Group call per-peer wraps are only decryptable by intended recipient
- "Sign once, wrap per recipient" produces identical inner event IDs
- SDP and ICE candidate special characters survive JSON+NIP-44 round-trip
- Ephemeral wrap keys are unique per wrap and differ from sender
- Inner event signatures are verifiable after unwrapping
- Full P2P call flow (all 7 signaling steps) through gift wraps
Uses real secp256k1 keys and NIP-44 encryption — no mocks.
https://claude.ai/code/session_01AfRYTRCvtKqqDxeKQujUrx
Add two previews: full layout with all slots (author picture, relay
badges, header rows, content, reactions) and a boosted variant without
author column or padding.
https://claude.ai/code/session_01Fam5VHNaNkKBBafg1gRfFS