Commit Graph

10892 Commits

Author SHA1 Message Date
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
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 47c92eb209 chore: ignore agent worktree directories
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
2026-04-06 14:56:12 +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 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
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 18b8df2d2d benchmark: 4×64-bit limbs — verify 3,954 ops/s (3.7x), sign 7,360 ops/s (2.1x)
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
2026-04-06 02:17:36 +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 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
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
Claude 78d8914ee1 test: add Android microbenchmark for secp256k1 operations
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
2026-04-05 20:32:56 +00:00
Claude 5b7f8478aa perf: secKeyVerify operates directly on bytes, avoids IntArray allocation
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
2026-04-05 20:32:56 +00:00
Claude de3808416c perf: pubKeyCompress now 2.4x faster than native — zero field arithmetic
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
2026-04-05 20:32:56 +00:00
Claude 57d8cc5d0c test: add 20 edge-case tests for GLV, FieldP, Point, and Secp256k1
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
2026-04-05 20:32:56 +00:00
Claude 7a96a11f6e refactor: extract GLV endomorphism and wNAF encoding into Glv.kt
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
2026-04-05 20:32:56 +00:00
Claude 3cbe6ee73b perf: reduce ByteArray allocations in verify and sign paths
- 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
2026-04-05 20:32:55 +00:00
Claude 0e3c2fcda5 docs: document Karatsuba attempt and why schoolbook is kept for 8 limbs
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
2026-04-05 20:32:55 +00:00
Claude 1f885accb1 perf: cache lambda(G) table, optimize P table build, reduce allocations
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
2026-04-05 20:32:55 +00:00
Claude 512ca0fec0 perf: optimized addition chains for field inversion and square root
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
2026-04-05 20:32:55 +00:00
Claude 7c9db9ca9c perf: implement GLV endomorphism — verify 68% faster (8.7x vs native)
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
2026-04-05 20:32:55 +00:00
Claude ec1833b346 perf: implement wNAF-5 Shamir for verify — 35% faster (10.6x vs native)
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
2026-04-05 20:32:55 +00:00
Claude e26ccb23cf test: add comprehensive unit tests for U256, FieldP, ScalarN, ECPoint
Tests each layer of the secp256k1 implementation independently, not just
through the public Secp256k1 API. This catches bugs in the foundations
that could silently produce wrong results for specific input values.

U256Test (22 tests):
- isZero: zero, non-zero, high-bit cases
- cmp: equal, less-than, greater-than
- addTo/subTo: simple, carry/borrow, overflow/underflow
- mulWide: small values, large values, sqrWide consistency
- sqrWide: matches mulWide for arbitrary and max values
- fromBytes/toBytes: round-trip, zero encoding
- getNibble: low nibbles, high nibbles
- testBit: low bits, high bits
- xorTo: basic XOR

FieldPTest (22 tests):
- Identities: add zero, sub self, mul one, neg twice
- Reduction: add near p, overflow past p, underflow past 0
- Commutativity, distributivity of mul
- sqr matches mul(a,a)
- inv: a * a^(-1) = 1, inv(1) = 1, inv(p-1) = p-1
- half: even values, odd values, half-then-double round-trip
- sqrt: square root of perfect square, non-residue rejection, generator point
- reduceWide: (p-1)² = 1 mod p
- In-place operations: add, sqr into output arrays

ScalarNTest (18 tests):
- isValid: normal, zero, n, n-1
- Identities: add zero, sub self, add/sub round-trip, neg twice, add neg
- Commutativity, distributivity of mul
- inv: a * a^(-1) = 1
- Edge cases: (n-1)+1=0, (n-1)+2=1, (n-1)²=1
- neg(0)=0, reduce(n)=0, reduce(small)=unchanged

PointTest (22 tests):
- Generator on curve: y² = x³ + 7
- doublePoint: matches 2·G, in-place, infinity
- addPoints: G+G=2G, infinity identity, inverse→infinity
- addMixed: matches full addition, infinity input
- mulG: by 1, by 0 (infinity), by n (infinity), matches generic mul
- mulDoubleG: separate vs combined computation
- liftX: generator, invalid x
- Serialization: compress/decompress round-trip, uncompressed round-trip, invalid key rejection

Total: 122 tests (86 new + 36 existing BIP-340/ACINQ vectors)

https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
2026-04-05 20:32:54 +00:00
Claude 368d1d16b7 refactor: split Field.kt into U256.kt, FieldP.kt, ScalarN.kt
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
2026-04-05 20:32:54 +00:00
Claude ff2a00587e refactor: document secp256k1 implementation and remove dead code
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
2026-04-05 20:32:54 +00:00
Claude 858cf2ea1a perf: add dedicated squaring, mixed Jac+Affine addition, optimized doubling
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
2026-04-05 20:32:54 +00:00
Claude fbd145557e test: add secp256k1 benchmark comparing native JNI vs pure Kotlin
Benchmarks all Secp256k1Instance operations: verifySchnorr, signSchnorr,
pubkeyCreate, pubKeyCompress, pubKeyTweakMul (ECDH), privKeyTweakAdd,
secKeyVerify, and the combined patterns used by the codebase.

Results on JVM:
  verifySchnorr    ~1,940 ops/s (Kotlin) vs ~25,000 ops/s (native) = ~13x
  signSchnorr      ~  820 ops/s (Kotlin) vs ~27,000 ops/s (native) = ~33x
  pubkeyCreate     ~3,130 ops/s (Kotlin) vs ~55,000 ops/s (native) = ~18x
  pubKeyTweakMul   ~2,740 ops/s (Kotlin) vs ~29,000 ops/s (native) = ~11x
  privKeyTweakAdd  ~1.66M ops/s (Kotlin) vs ~1.46M ops/s (native)  = 0.9x (faster!)
  secKeyVerify     ~1.98M ops/s (Kotlin) vs ~3.88M ops/s (native)  = 2x

https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
2026-04-05 20:32:54 +00:00
Claude 4d55dfcff5 perf: optimize secp256k1 for minimum allocations and maximum verify throughput
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
2026-04-05 20:32:54 +00:00
Claude 4c3b31fe8e feat: replace fr.acinq.secp256k1 with pure-Kotlin secp256k1 implementation
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
2026-04-05 20:32:54 +00:00
Vitor Pamplona e1304ddcab Merge pull request #2147 from vitorpamplona/claude/marmot-protocol-integration-xb3TT
Add Marmot MLS group messaging support to Amethyst
2026-04-05 16:06:12 -04:00
Claude 8b56b3b3bf feat: Phase 6 — Marmot account integration & event routing
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
2026-04-05 19:54:56 +00:00
Vitor Pamplona 3b695dc8b8 Merge pull request #2145 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-04-05 15:19:18 -04:00