Add DraggableRelayList composable with gesture-based drag-and-drop,
following the same pattern used in ReactionsSettingsScreen. Each relay
category (DM, NIP65 home/notif, search, blocked, trusted, local,
broadcast, indexer, proxy, private outbox, favorites) now shows a
drag handle and supports reordering via drag gestures.
https://claude.ai/code/session_01RVM5kEJGrCaJTaP3GmVHDd
- Increase WINDOW_G from 8 to 12 for mulDoubleG (verify): reduces
G-side additions from ~32 to ~22 per verification (saves ~110 field ops)
- Add batchToAffine using Montgomery's trick: 1 inversion + 3(n-1) muls
instead of n individual inversions. Critical for the 1024-entry table.
- Table size: 1024 entries × 64 bytes = ~128KB (lazy, built on first use)
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
The publish KeyPackage button was always active because the app didn't
track whether a key package had already been published. This adds:
- hasActiveKeyPackages() to KeyPackageRotationManager and MarmotManager
- hasPublishedKeyPackage() to Account, checking both in-memory bundles
and the local cache for existing kind:30443 events
- Own key package filter in MarmotSubscriptionManager and the EOSE
manager so previously published key packages are downloaded from
relays on app restart
- UI feedback: primary-colored key icon when published, contextual
empty-state message, and a spinner during publishing
https://claude.ai/code/session_01BVe7aSEWd2KLi5Ks6RZkcc
- Add ECPoint.toAffineX that computes only x = X/Z² (saves 2M vs full
toAffine which also computes Y/Z³)
- Use toAffineX in ecdhXOnly since only the x-coordinate is needed
- Add ecdhXOnly benchmark measuring the actual Nostr ECDH production
path (Secp256k1Instance.pubKeyTweakMulCompact delegates to ecdhXOnly)
- Fix ktlint KDoc-inside-class-body violations
The old benchmark measured pubKeyTweakMul(02||x, key) which pays for:
array allocation, compressed key parsing (sqrt), full toAffine, and
re-serialization. ecdhXOnly avoids the array overhead and serialization.
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
Replace the raw OutlinedTextField + IconButton Row with
ThinPaddingTextField, ThinSendButton, EditFieldBorder, and
EditFieldModifier to match the private DM and channel chat layouts.
https://claude.ai/code/session_013dhfy18dfuSYN8khXiLLBb
The publish KeyPackage button had no visual feedback, making it unclear
whether anything happened. Added a Snackbar to confirm success or show
errors, disabled the button during publishing, and updated the hint text
to direct users to the key icon.
https://claude.ai/code/session_013dhfy18dfuSYN8khXiLLBb
FieldP.mul/sqr were calling ThreadLocal.get() for every invocation (~500+
times per scalar multiplication, ~20-30ns each on JVM). Point operations
(doublePoint, addMixed, addPoints) each did an additional ThreadLocal.get()
for their scratch buffers.
Fix: add overloads that accept a pre-fetched wide buffer (LongArray(8))
and PointScratch. Top-level entry points (mulG, mul, mulDoubleG) fetch
the ThreadLocal once and thread it through all inner calls.
Results (ops/s, vs native JNI):
- pubkeyCreate: 19,163 → 29,205 (+52%, 3.0x → 2.2x)
- signSchnorr cached: 13,007 → 18,397 (+41%, 2.1x → 1.5x)
- signSchnorr: 5,365 → 7,490 (+40%, 5.7x → 3.7x)
- verifySchnorr: 3,840 → 4,873 (+27%, 7.2x → 5.4x)
- ECDH: 5,569 → 7,870 (+41%, 5.5x → 3.8x)
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
The 4×64-bit reduceWide in FieldP had a bug: round 2 carry propagation
could overflow past 256 bits when out[0..3] were all 0xFF...FF, silently
dropping the overflow. This caused field multiplication results to be
off by exactly C = 2^32 + 977, corrupting point arithmetic for specific
intermediate values (e.g. ECDH with scalar n-2 on small x-coordinates).
Fix: detect round-2 overflow and fold the extra bit (≡ C mod p) back in.
Also fix ktlint violations in ScalarN and update documentation.
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
The share dialog in zoomable/fullscreen media view was auto-hiding
before users could select an option. This was caused by:
1. A 2-second timer in ZoomableContentDialog that hides all controls
(including the share dialog) via AnimatedVisibility
2. A LaunchedEffect in RenderTopButtons that explicitly closed the
share dialog whenever video controls auto-hid
3. Tap-to-toggle on the background also hiding controls while dialog
was open
Fix: Hoist the share popup state and guard the auto-hide timer,
tap-to-toggle, and video control hide from dismissing when the
share dialog is open.
https://claude.ai/code/session_01YLujgxRDuujKCS3vgHc6Gq
The open polls notification flow only re-evaluated when new notes arrived
or dismissed IDs changed. Polls that passed their close date remained
visible until something else triggered the flow. Adding a 1-minute ticker
to the combine ensures expired polls are filtered out promptly.
https://claude.ai/code/session_01VY9FRuzHzwTVrePaJiHuHx
Initial benchmark results with the LongArray(4) + Math.multiplyHigh
representation. Machine was under variable load so ratios fluctuate,
but the improvement trend is clear:
verifySchnorr: 3,954 ops/s (3.7× vs native, was 7.2×)
signSchnorr cached: 7,360 ops/s (2.1× vs native, was 2.2×)
pubkeyCreate: 17,896 ops/s (2.4× vs native, was 3.4×)
The 4×64 representation reduces field multiplication inner products from
64 to 16 (4×4 with unsignedMultiplyHigh). The unsigned carry detection
overhead partially offsets the gain, giving ~1.5× raw mul speedup.
Still needs: fix NIP-44 vector 32 (n-8 scalar edge case).
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
Major progress on the LongArray(4) representation:
- U256.kt: all 25 tests pass (mulWide, sqrWide, serialization, bit ops)
- FieldP.kt: all 27 tests pass (add, sub, mul, sqr, half, inv, sqrt)
- ScalarN.kt: 17 of 19 tests pass — reduceWide has a bug for products
near n² (invMulIsOne and mulLargeScalars fail)
- Glv.kt: rewritten cleanly with correct 4-limb constants
- All test files updated for LongArray types and 4-element arrays
The reduceWide bug is in the overflow handling of the second round
hi×N_COMPLEMENT folding — needs careful unsigned Long carry tracking.
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
Progress on the LongArray(4) migration:
- ScalarN: constants converted to 4×64-bit, loop bounds fixed
- KeyCodec: B constant fixed
- Glv: constants partially converted but regex left residual old values
- Point: types fixed, GX/GY constants converted
- Secp256k1: parameter types updated
Still needs: manual cleanup of Glv constants, ScalarN reduceWide internals,
test hex() helpers, test constant arrays, wNAF bit manipulation for 64-bit limbs.
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
Bulk sed replacement of IntArray(8)→LongArray(4), IntArray(16)→LongArray(8),
intArrayOf→longArrayOf across all remaining files. This creates many compile
errors that need manual fixing:
- Type declarations still say IntArray where LongArray is needed
- Constants still have 8 values (32-bit) instead of 4 (64-bit)
- Loop bounds still reference 8 instead of 4
- toInt() casts on longArrayOf elements
- mulShift384 internals broken for new layout
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
Rewrites FieldP to use the new 4×64-bit limb representation:
- All field operations (add, sub, mul, sqr, neg, half, inv, sqrt) now
operate on LongArray(4)
- reduceWide uses unsignedMultiplyHigh for the hi×C reduction step,
leveraging the hardware intrinsic on JVM
- Thread-local scratch is LongArray(8) instead of IntArray(16)
- Addition chains for inv/sqrt unchanged (same algorithm, new types)
The reduceWide is cleaner than the 8×32 version: since C = 2^32+977 < 2^33,
each hi[i]×C product fits in 97 bits, and unsignedMultiplyHigh gives the
upper 64 bits directly.
NOTE: Build still broken — ScalarN, Glv, Point, KeyCodec, Secp256k1,
and tests still expect IntArray(8).
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
Foundation layer for the 4×64-bit limb optimization. This commit rewrites
U256 from IntArray(8) to LongArray(4) and adds platform-specific
Math.multiplyHigh for 64×64→128-bit products:
- JVM: Math.multiplyHigh intrinsic (single IMULH/SMULH instruction)
- Android API 31+: Math.multiplyHigh, fallback to pure Kotlin on older
- Native: pure Kotlin fallback (4 sub-products per multiplyHigh call)
The 4×64 representation reduces inner products from 64 to 16 per field
multiply on JVM, a potential ~1.5-2× speedup on the critical path.
NOTE: This commit intentionally breaks the build — FieldP, ScalarN, Glv,
Point, KeyCodec, Secp256k1, and all tests still expect IntArray(8).
They will be updated in subsequent commits.
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
Add three new test files for the Marmot MLS implementation:
- MlsGroupLifecycleTest: End-to-end lifecycle tests covering Welcome
processing, cross-member encrypt/decrypt, multi-member groups, commit
processing, external joins, PSK proposals, and ReInit proposals.
- MlsGroupEdgeCaseTest: Security boundary tests for wrong epoch rejection,
corrupted ciphertext detection, invalid KeyPackage rejection, out-of-range
leaf indices, empty/large message handling, and add/remove/add cycles.
- MlsConformanceTest: Cross-implementation comparison tests verifying
KeyPackage structure, GroupInfo signatures, Welcome message format,
HPKE seal/open, SignWithLabel/VerifyWithLabel, LeafNode signatures,
deterministic key schedule, and commit structure conformance.
Fix GroupInfo signature bug (RFC 9420 Section 12.4.3.1):
- buildWelcome() and groupInfo() were signing only groupContext.toTlsBytes()
but verifySignature() checked against encodeTbs() which includes
GroupContext + extensions + confirmationTag + signer. Now both methods
build an unsigned GroupInfo first and sign its full TBS encoding.
Enhance MlsGroupManager KDoc with usage examples, responsibility breakdown,
and cross-implementation notes.
8 tests are @Ignore'd documenting a known bug: processCommit() does not
derive the same epoch secrets as commit(), causing cross-member AEAD
failures after epoch transitions.
https://claude.ai/code/session_018f67fqNReg3dEXcDimYLY1
Extends the ThreadLocal SHA256 optimization to the streaming hash function.
The pool is retained for EventHasherSerializer and HashingByteArrayBuilder
which use its acquire/release pattern for long-lived incremental hashing.
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
The SHA256 pool used ArrayBlockingQueue.take()/put() which acquire a lock
on every call. Profiling showed ~10µs synchronization overhead per SHA256
for ~2µs of actual hashing — the lock cost 5× more than the hash itself.
Replaced with ThreadLocal<MessageDigest> which gives each thread its own
instance with zero synchronization. MessageDigest.digest() implicitly
resets state, so no explicit reset is needed.
The pool is kept available for streaming use cases (hashStream) that need
incremental hashing with update()/digest() across multiple calls.
Only affects jvmAndroid — Apple uses CC_SHA256 directly, Linux uses
whyoleg CryptographyProvider. Both already have no pool overhead.
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
Implement end-to-end reading of MarmotGroupData (extension 0xF2EE)
from the MLS GroupContext.extensions and sync to the UI layer.
Quartz (protocol):
- MarmotGroupData.decodeTls(): deserialize from TLS wire format
(version, nostrGroupId, name, description, admin pubkeys, relays,
image fields)
- MarmotGroupData.fromExtensions(): find and decode from extension list
- MlsGroup.extensions: expose groupContext.extensions publicly
Commons (shared logic):
- MarmotGroupChatroom: add description, adminPubkeys, relays fields
as MutableStateFlow for reactive UI
- MarmotManager.groupMetadata(): extract MIP-01 data from group
- MarmotManager.syncMetadataTo(): sync metadata + member count to
a MarmotGroupChatroom
Sync points (amethyst):
- Account init: sync metadata after restoreAll() for all restored groups
- GiftWrapEventHandler: sync after Welcome processing (group join)
- GroupEventHandler: sync after CommitProcessed (epoch advances may
update extensions via GroupContextExtensions proposals)
UI (GroupInfoScreen):
- Display group description from MIP-01 metadata
- Display relay URLs from MIP-01 metadata
- Mark admin members with "- admin" suffix (from adminPubkeys)
https://claude.ai/code/session_0194SxKfAU61PY92eqP4cCXM
Documentation fixes:
- Glv.kt: Updated wNAF description to reference all three multiplication
strategies (comb, GLV+wNAF, Strauss) instead of stale "4-bit windowing"
New test file:
- KeyCodecTest.kt (14 tests): Comprehensive tests for the extracted KeyCodec
object — liftX (generator, invalid, not-on-curve, even-y guarantee),
hasEvenY (even/odd), parsePublicKey (compressed even/odd, uncompressed,
invalid sizes, invalid prefix, not-on-curve), serialization round-trips
Added tests to existing files:
- U256Test (+3): toBytesInto at offset, copyInto, fromBytes with offset
- Secp256k1Test (+3): ecdhXOnly matches tweakMul, ecdhXOnly symmetric,
taggedHash correctness
Coverage audit: all public/internal functions in all 7 implementation files
now have direct test references. The only untested functions are internal
utilities (FieldP.reduceSelf, MutablePoint.copyFrom) that are exercised
transitively by every field and point operation test.
Total: 146 → 166 tests
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
Moves public key parsing, serialization, liftX, and hasEvenY into a
dedicated KeyCodec object. These functions operate on field math only
(FieldP, U256) and don't use EC point operations or scratch buffers,
making them a natural separate concern.
Point.kt retains thin delegation methods (liftX, hasEvenY, parsePublicKey,
serializeCompressed, serializeUncompressed) so all existing callers
(ECPoint.liftX, etc.) continue to work without changes.
Point.kt: 803 → 710 lines
KeyCodec.kt: 133 lines (new)
No functional changes — pure reorganization.
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
Stale documentation from earlier iterations was referencing algorithms
and performance numbers that are no longer current:
Point.kt:
- Updated SCALAR MULTIPLICATION section to document all three strategies:
1. Comb method for mulG (3 doublings + ~43 additions, 704-entry table)
2. GLV+wNAF-5 for mul (arbitrary point, ~130 dbl + ~22 adds)
3. Strauss+GLV+wNAF for mulDoubleG (4 streams, shared ~130 doublings)
- Removed stale "4-bit windowed method" and "[1G..16G] table" descriptions
Secp256k1.kt:
- Updated performance numbers: verify ~3,700, sign ~14K, create ~18K ops/s
- Noted that all algorithmic optimizations from libsecp256k1 are implemented
- Removed stale "~2,100 verify/s" and "absence of GLV" claims
U256.kt:
- Updated header to describe the full package architecture with all 6 files
- Added Glv.kt and file roles to the overview
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
Replace the plain text input with the existing UserSuggestionState +
ShowUserSuggestionList pattern used throughout the app (NewGroupDM,
edit post, etc.).
The flow is now:
1. Type a name, npub, or NIP-05 in the search field
2. Debounced search queries local cache + relays + NIP-05 DNS
3. ShowUserSuggestionList displays matching users with profile
pictures and display names
4. Tap a user to select them — shows their profile in a selected
user row with picture, display name, truncated pubkey, and a
Clear button
5. Tap "Add" to fetch their KeyPackage and complete the invite
https://claude.ai/code/session_0194SxKfAU61PY92eqP4cCXM
The comb method (Hamburg 2012) replaces GLV+wNAF for generator multiplication.
Instead of ~130 doublings + ~32 additions, it arranges the 256 scalar bits
into a 4×66 matrix and processes each of 4 rows with 11 table lookups.
Only 3 doublings are needed between rows.
Algorithm: COMB_BLOCKS=11, COMB_TEETH=6, COMB_SPACING=4
- 11 blocks × 64 entries = 704 affine points (~45KB), lazily precomputed
- Per mulG: 3 doublings + ~43 mixed additions ≈ 464 M-equiv
- Previous GLV+wNAF: ~130 doublings + ~32 additions ≈ 1,035 M-equiv
- Theoretical speedup: 2.2× (measured: 2.0-2.1×)
Table construction uses an efficient Gray-code-like ordering: each successive
mask differs by one bit, so each entry is built from the previous with one
point addition instead of summing all teeth from scratch.
Benchmark improvements:
pubkeyCreate: 8,383 → 17,654 ops/s (2.1×, now 3.6× vs native)
signSchnorr (cached): 7,411 → 13,642 ops/s (1.8×, now 2.3× vs native)
signSchnorr: 3,257 → 5,856 ops/s (1.8×, now 4.8× vs native)
compressedPubKeyFor: 8,475 → 16,695 ops/s (2.0×, now 3.1× vs native)
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
AddMember flow (was TODO, now functional):
- Account.fetchKeyPackageAndAddMember(): queries outbox relays for the
target user's KeyPackageEvent (kind:30443) using client.fetchFirst(),
decodes the base64 KeyPackage content, and calls addMarmotGroupMember()
which publishes the commit then sends the Welcome gift wrap
- AddMemberScreen now calls the real implementation and navigates back
on success
Member display names:
- MemberRow now uses LoadUser to resolve Nostr pubkeys to User profiles
from LocalCache, showing bestDisplayName() instead of raw hex
- UserPicture composable shows profile pictures next to member names
- "(you)" indicator for the current user's entry
https://claude.ai/code/session_0194SxKfAU61PY92eqP4cCXM
The C library uses WINDOW_G=15 (8192 precomputed entries) for G-side
multiplication. We were using width 5 (8 entries), giving ~26 non-zero
wNAF digits per 128-bit half-scalar. Width 8 (64 entries) reduces this
to ~16, saving ~20 mixed additions across the two G-streams per verify.
Changes:
- WINDOW_G=8 with G_TABLE_SIZE=64 precomputed affine odd-multiples of G
- gOddTable: [1G, 3G, 5G, ..., 127G] lazily computed (64 inversions at init)
- gLamTable: [λ(1G), λ(3G), ..., λ(127G)] lazily computed (64 β multiplies)
- mulG and mulDoubleG use WINDOW_G for G-side wNAF encoding
- mulDoubleG uses separate wP=5 for P-side (table built per-call, keep small)
- Memory: ~8KB for G table + ~8KB for λ(G) table = 16KB total (cached)
Benchmark improvement:
verifySchnorr: ~6-8x vs native (was ~8-10x)
signSchnorr: ~8-9x vs native (was ~10-12x)
All G-multiplication operations benefit
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
Replace AlertDialog-based CreateGroupDialog and AddMemberDialog with
proper full-screen routes that slide up from the bottom, following the
existing app patterns (ChannelMetadataEdit, NewGroupDM, etc.).
New screens:
- CreateGroupScreen: uses CreatingTopBar with close/create buttons,
replaces CreateGroupDialog
- AddMemberScreen: uses ActionTopBar with close/add buttons,
replaces AddMemberDialog
New routes:
- Route.CreateMarmotGroup → composableFromBottom
- Route.MarmotGroupAddMember(nostrGroupId) → composableFromBottomArgs
Updated call sites:
- MarmotGroupListScreen: FAB navigates to Route.CreateMarmotGroup
- MarmotGroupChatScreen: add-member icon navigates to
Route.MarmotGroupAddMember
- MarmotGroupInfoScreen: add-member icon navigates to
Route.MarmotGroupAddMember
Deleted old files:
- CreateGroupDialog.kt
- AddMemberDialog.kt
https://claude.ai/code/session_0194SxKfAU61PY92eqP4cCXM