Commit Graph

1652 Commits

Author SHA1 Message Date
Vitor Pamplona fe5f07da3f Moves the hex operations to our own functions 2026-04-06 17:55:54 -04:00
Vitor Pamplona 094586ff41 Fixes the sign issue in the fallback function 2026-04-06 17:55:17 -04:00
Vitor Pamplona 7a8b7a5e9f Merge branch 'claude/port-secp256k1-kotlin-Ir8yz' of https://github.com/vitorpamplona/amethyst into claude/port-secp256k1-kotlin-Ir8yz 2026-04-06 17:13:21 -04:00
Vitor Pamplona 57eb0ef803 Merge branch 'main' into claude/port-secp256k1-kotlin-Ir8yz 2026-04-06 17:13:01 -04:00
Vitor Pamplona 8a914d3130 removes some warnings 2026-04-06 17:12:00 -04:00
Claude 034c1ab2d1 fix: replace ThreadLocal with KMP-compatible ScratchLocal expect/actual
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
2026-04-06 21:06:44 +00:00
Vitor Pamplona b60a0bcae8 Reverts the use of our own Secp256K1 until faster than native 2026-04-06 16:31:38 -04:00
Claude e0fc42e955 perf: Android API-level-gated unsignedMultiplyHigh + eliminate per-call branch
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
2026-04-06 20:14:45 +00:00
Claude 2bf911379f docs: update Secp256k1.kt with instruction-level C comparison
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
2026-04-06 19:58:51 +00:00
Claude f9ec821107 docs+cleanup: inline hot-path functions, fix stale comments
- 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
2026-04-06 19:42:47 +00:00
Claude 494ca22bdf perf: pre-allocate inv/sqrt addition chain scratch via ThreadLocal
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
2026-04-06 19:28:48 +00:00
Claude 20bab17440 perf: pre-allocate P-side tables and batch inversion temps in PointScratch
Eliminates ~80 LongArray allocations per mul/mulDoubleG call by
pre-allocating the P-side Jacobian and affine tables, the doubling
temp, and the batch inversion scratch buffers in PointScratch
(thread-local, allocated once per thread, reused across calls).

Before: mul() allocated 8 MutablePoint (24 LongArray) + 8 MutablePoint
(24 LongArray) + 16 AffinePoint (32 LongArray) + batch temps = ~92
LongArray per call. After: 0 allocations in the table construction path.

Also fixes minor allocation in addMixed degenerate case (use t[5]
scratch instead of new LongArray(4)).

https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
2026-04-06 19:22:38 +00:00
Claude 09427fe6cf perf: direct Math.unsignedMultiplyHigh call — eliminate MethodHandle boxing
The JVM target is Java 21, so Math.unsignedMultiplyHigh (Java 18+) can
be called directly without MethodHandle reflection. The previous approach
used MethodHandle.invokeExact which Kotlin compiles with Object return
type, causing Long boxing on every call (3 box/unbox per invocation ×
16 calls per field multiply = 48 boxed objects per mul).

Direct call compiles to a single UMULH instruction with zero overhead.
This is the most performance-critical function: called ~12,000× per
signature verification.

https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
2026-04-06 19:01:38 +00:00
Claude 7e8a060f17 perf: optimize reduceSelf for secp256k1, pre-allocate wNAF scratch
Two microoptimizations:

1. reduceSelf: exploit P's structure (P[1..3] = 0xFFFFFFFFFFFFFFFF).
   a >= P only if all top 3 limbs are max AND a[0] >= P[0]. The first
   check (a[3] == -1) fails >99.99% of the time, making this a single
   branch miss prediction instead of a 4-limb comparison loop.
   Called ~1,300× per verify, ~500× per ECDH.

2. Pre-allocate wNAF IntArrays and scratch MutablePoint/LongArray in
   PointScratch. Eliminates 8-12 IntArray(145) + 8-12 LongArray(4)
   allocations per mul/mulDoubleG call. Adds wnafInto() to Glv that
   writes into caller-provided arrays.

https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
2026-04-06 18:33:15 +00:00
Claude e256fca772 perf: shared Z inversion for GLV table pairs — saves one full inversion
pOdd and pLamOdd always have identical Z coordinates (the GLV
endomorphism λ(X,Y,Z) = (β·X, Y, Z) preserves Z). Previously we
called batchToAffine separately for each table, paying two full field
inversions (~270 field ops each). Now batchToAffinePair uses a single
batch inversion and reuses the Z⁻¹ values for both tables.

Saves ~270 field ops per mul/mulDoubleG call (~12% of ECDH cost).

https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
2026-04-06 18:18:48 +00:00
Claude 1b9706c23f perf: use Math.unsignedMultiplyHigh on Java 18+ — up to 61% faster
On Java 18+, Math.unsignedMultiplyHigh compiles to a single UMULH
instruction, eliminating the 4-instruction signed→unsigned correction
(multiplyHigh + 2 AND + 1 SHR + 2 ADD) per 64×64 product. With 16
products per field multiply, this saves ~64 instructions per mul/sqr.

Detection uses MethodHandle resolved once at class init. On older JVMs
and Android, falls back to the signed+correction path automatically.

Results on Java 21 (vs previous):
  pubkeyCreate:  23,400 → 37,700 ops/s (+61%, 2.2× → 1.6× native)
  sign (cached): 16,700 → 27,400 ops/s (+64%, 1.5× → 1.1× native)
  verify:         5,600 →  7,300 ops/s (+31%, 4.4× → 3.6× native)
  ECDH:           7,100 → 10,000 ops/s (+41%, 3.9× → 3.5× native)
  ecdhXOnly:      5,600 → 10,500 ops/s (+88%, 4.4× → 2.6× native)

https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
2026-04-06 17:58:02 +00:00
Claude ef7790b775 docs: update secp256k1 documentation with benchmark results and architecture notes
Update KDoc and file headers across Secp256k1.kt, Point.kt, FieldP.kt,
and U256.kt with:
- Accurate benchmark numbers (well-warmed: verify 4.4x, sign 1.5x,
  pubCreate 2.2x, ECDH 3.9x, compress 2x FASTER)
- Detailed comparison with C libsecp256k1 architecture choices
- Explanation of why certain C optimizations don't port to JVM:
  * Lazy reduction: 4x64 limbs have no headroom (C's 5x52 has 12 bits)
  * safegcd: slower on JVM due to 128-bit arithmetic overhead
  * WINDOW_G=15: cache pressure from heap-allocated objects (w=12 optimal)
- Document effective-affine technique and batch inversion
- Increase all benchmark warmup/iterations for stable measurements

https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
2026-04-06 17:32:50 +00:00
Claude beaf452d55 perf: keep WINDOW_G=12, increase benchmark warmup
Tested WINDOW_G=15 (matching C libsecp256k1): 5.6x slower than native.
WINDOW_G=12 is faster at 4.3x because the 8192-entry table (1MB) at
w=15 causes cache pressure on JVM — heap-allocated AffinePoint objects
are scattered in memory, unlike C's contiguous compile-time .rodata.
WINDOW_G=12 (1024 entries, ~128KB) fits comfortably in L2 cache.

Increased verify benchmark warmup to 200+500 for stable measurements
(first call builds the lazy table).

https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
2026-04-06 17:23:13 +00:00
Claude 69ee4bd11d revert: remove safegcd — Fermat chain is faster on JVM
The safegcd (Bernstein-Yang divsteps) algorithm is faster than Fermat
in C due to native 128-bit integer support, but on JVM the 128-bit
arithmetic overhead via multiplyHigh + carry tracking in the inner
loop (12 rounds × matrix multiply on 5 limbs) is slower than the
Fermat addition chain (255 sqr + 15 mul of optimized field ops).

Benchmark showed 8.3x vs native (was 5.0x with Fermat), confirming
that the per-operation constant factor matters more than algorithmic
complexity for this problem size on JVM.

https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
2026-04-06 16:50:33 +00:00
Claude 59df3162a6 perf: implement safegcd modular inverse (Bernstein-Yang 2019)
Replace Fermat's little theorem (a^(p-2), 255 sqr + 15 mul) with the
safegcd divsteps algorithm for field element inversion. This processes
62 divsteps per batch using a 2×2 transition matrix applied to
full-precision values via 128-bit arithmetic (multiplyHigh).

12 rounds × 62 steps = 744 total divsteps (≥741 needed for 256 bits).
Uses 5×62-bit signed limb representation for intermediate values and
Montgomery-style correction (precomputed p^{-1} mod 2^62) for the
modular reduction in updateDE.

The old Fermat chain is preserved as FieldP.invFermat for reference.

https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
2026-04-06 16:47:17 +00:00
Vitor Pamplona 5a36d0b7ae Code clean up and building a common parent class for WebRTC calls 2026-04-06 12:02:08 -04:00
Claude cb78f1a34e bench: increase warmup for verify/ECDH benchmarks
More warmup iterations needed for WINDOW_G=12 (1024-entry table
built lazily on first call). Avoids table-build cost contaminating
measured iterations.

https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
2026-04-06 15:09:33 +00:00
Claude 80dc164c7c perf: increase G window to 12, add batch inversion for table building
- 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
2026-04-06 14:55:14 +00:00
Claude 66e6b0b726 fix: track KeyPackage published status and subscribe to own key packages
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
2026-04-06 14:43:06 +00:00
Claude 2661bf783d perf: add toAffineX for x-only ECDH, benchmark ecdhXOnly path
- 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
2026-04-06 14:27:30 +00:00
Claude 547be89577 perf: eliminate ThreadLocal.get() from hot paths — 27-52% faster across all EC ops
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
2026-04-06 13:38:01 +00:00
Vitor Pamplona 014e08fa0d Fixes some warnings 2026-04-06 09:26:04 -04:00
Vitor Pamplona a1145e2a28 Fixing hex and time functions 2026-04-06 09:23:51 -04:00
Claude 36a7ae147e fix: reduceWide round-2 carry overflow — fixes NIP-44 conversation key for edge-case scalars
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
2026-04-06 13:12:18 +00:00
Claude cb1e8be529 feat: complete 4×64-bit LongArray migration — 167/167 secp256k1 tests pass
All secp256k1 tests pass with the new 4×64-bit limb representation:
- U256: 25/25 pass (mulWide, sqrWide with unsignedMultiplyHigh)
- FieldP: 27/27 pass (reduceWide using unsignedMultiplyHigh for C-multiply)
- ScalarN: 19/19 pass (reduceWide with correct unsigned overflow handling)
- Glv: 14/14 pass (wNAF encoding with 64-bit limb bit manipulation)
- Point: 22/22 pass (comb method, Strauss, all scalar mul strategies)
- Schnorr: 22/22 pass (all BIP-340 test vectors)
- Secp256k1: 20/20 pass (key ops, ECDH, sign/verify)
- KeyCodec: 18/18 pass

One downstream failure: NIP-44 conversation key vector 32 (scalar n-8)
needs investigation — likely a ScalarN edge case in GLV decomposition.

https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
2026-04-06 02:12:36 +00:00
Claude f1d7125fac feat: 4×64-bit migration — U256 and FieldP pass all tests, ScalarN has reduceWide bug
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
2026-04-06 02:01:17 +00:00
Claude 689c52ed6f feat: fix constants and types in ScalarN, KeyCodec, partial Glv fix (WIP — still broken)
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
2026-04-06 01:18:40 +00:00
Claude 69f222c6d5 feat: mechanical IntArray→LongArray replacement (WIP — broken, needs manual fixes)
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
2026-04-06 01:14:04 +00:00
Claude 55016ff093 feat: rewrite FieldP for LongArray(4) limbs (WIP — breaks build)
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
2026-04-06 01:12:56 +00:00
Claude 040b4ce02a feat: 4×64-bit limb representation with Math.multiplyHigh (WIP — breaks build)
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
2026-04-06 01:10:10 +00:00
Claude 8379892078 feat: improve MLS implementation with comprehensive tests, docs, and bug fix
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
2026-04-05 23:39:06 +00:00
Claude 9a8ed53279 perf: sha256StreamWithCount uses ThreadLocal instead of pool
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
2026-04-05 23:34:49 +00:00
Claude ecac49f9f0 perf: replace SHA256 pool with ThreadLocal — eliminates lock overhead
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
2026-04-05 22:57:04 +00:00
Vitor Pamplona 4435073234 Merge pull request #2149 from vitorpamplona/claude/group-chat-ui-viewmodel-5WKMV
Add Marmot MLS group messaging UI and integration
2026-04-05 18:43:38 -04:00
Claude 3e49e650c2 feat: read MIP-01 group metadata from MLS GroupContext extensions
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
2026-04-05 22:10:33 +00:00
Claude 1c1c73a0dd docs+test: fix stale docs, add KeyCodecTest and 20 new edge-case tests
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
2026-04-05 22:03:56 +00:00
Claude fe73b9561c refactor: extract KeyCodec.kt from Point.kt for key encoding/decoding
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
2026-04-05 21:57:07 +00:00
Claude 0830706324 docs: update all file headers to reflect current optimization state
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
2026-04-05 21:53:20 +00:00
Claude 57b0219a71 perf: implement comb method for G multiplication — 2x faster pubkeyCreate/sign
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
2026-04-05 21:46:09 +00:00
Claude 662d31dff5 perf: increase G-side wNAF window from 5 to 8 — fewer G additions per verify
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
2026-04-05 21:33:54 +00:00
Claude f6e77fba24 perf: add ecdhXOnly for direct x-only ECDH, skip intermediate allocations
Adds Secp256k1.ecdhXOnly(xOnlyPub, scalar) that directly computes the
x-coordinate of scalar·P from a 32-byte x-only public key. This replaces
the previous pubKeyTweakMulCompact path that went through:
  h02 + pubKey → pubKeyTweakMul → serializeCompressed → copyOfRange(1,33)

The new path eliminates 4 ByteArray allocations per call (h02 concat,
parsePublicKey's copyOfRange, serializeCompressed, final copyOfRange).

The square root for y-decompression is still needed (EC point operations
require both coordinates), but the x-coordinate of the result is the same
regardless of y sign since k·(-P) = -(k·P) and negation preserves x.

A Montgomery ladder (x-only arithmetic without y) would eliminate the sqrt
entirely but requires a complete algorithm rewrite.

Analysis of remaining pubKeyTweakMul cost vs C:
- sqrt for y-decompression: ~267 ops (C doesn't need — key already parsed)
- inv for Jacobian→affine: ~270 ops (both C and Kotlin do this)
- 8×32 limbs: 64 products/mul vs C's 25 (JVM ceiling)
- Full Jacobian P-side addition: 11M+5S vs C's mixed 8M+3S

https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
2026-04-05 21:18:48 +00:00
Claude 629450fed2 perf: signSchnorr 4.8x faster — remove self-verify, add cached-pubkey path
Analysis of the C library's schnorrsig_sign showed three key differences:
1. C takes a pre-computed keypair (no pubkey derivation during signing)
2. C does NOT self-verify the signature after signing
3. C does 1 EC multiplication total; we were doing 3

Changes:
- signSchnorrInternal: extracted core signing logic without pubkey derivation
  or self-verification. Does exactly 1 mulG (for R = k·G) matching the C library.
- signSchnorr: convenience overload that derives pubkey then calls internal.
  Now ~2x faster since self-verify is removed.
- signSchnorrWithPubKey: fast path accepting a 33-byte compressed pubkey
  (includes y-parity in the 02/03 prefix). Skips the pubkey G multiplication
  entirely. ~4.8x faster than the previous signSchnorr.
- Secp256k1Instance: added signSchnorrWithPubKey forwarding method.
- Benchmark: added "signSchnorr (cached pk)" test comparing both paths.

The self-verify removal is safe: the BIP-340 test vectors (which include
the exact expected signatures) validate correctness, and the C reference
library does not self-verify either.

Benchmark:
  signSchnorr:            1,516 → 2,915 ops/s (1.9x faster, 17x → 9.5x)
  signSchnorr (cached pk):  N/A → 7,261 ops/s (3.9x vs native — new!)

https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
2026-04-05 21:12:59 +00:00
Claude 2dab449d62 perf: apply GLV+wNAF to mul and mulG — all scalar muls now half-speed
Previously only mulDoubleG (verification) used GLV endomorphism + wNAF-5.
The generic mul() and mulG() still used the old 4-bit window with 256
doublings and full Jacobian addition. Now all three scalar multiplication
functions use GLV to split scalars into ~128-bit halves.

mul() (arbitrary point, used by pubKeyTweakMul/ECDH):
- Was: 4-bit window, 256 doublings, 15-entry Jacobian table, ~60 full adds
- Now: GLV split + wNAF-5, ~130 doublings, 2×8-entry Jacobian tables

mulG() (generator point, used by pubkeyCreate and signSchnorr):
- Was: 4-bit window, 256 doublings, precomputed affine table, ~60 mixed adds
- Now: GLV split + wNAF-5, ~130 doublings, precomputed affine G + λ(G) tables

Benchmark improvements:
  pubkeyCreate:         4,391 → 6,689 ops/s (52% faster, 12.5x → 7.7x)
  pubKeyTweakMul:       3,427 → 5,233 ops/s (53% faster, 8.6x → 6.5x)
  pubKeyTweakMulCompact:2,941 → 4,593 ops/s (56% faster, 8.7x → 5.6x)
  signSchnorr:          1,178 → 1,516 ops/s (29% faster, 23.1x → 17.3x)
  compressedPubKeyFor:  4,358 → 6,919 ops/s (59% faster, 12.3x → 7.1x)

https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
2026-04-05 21:03:39 +00:00
Vitor Pamplona 458443f156 Fixes typemismatch 2026-04-05 16:45:47 -04:00
Vitor Pamplona 4a416abdc9 Merge pull request #2148 from greenart7c3/claude/add-alt-tag-payment-eyb1R
Add ALT tag support to PaymentTargetsEvent
2026-04-05 16:39:59 -04:00