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
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
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
- 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
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
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
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
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
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
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
Adds ALT constant, FIXED_D_TAG constant, and AltTag import to
PaymentTargetsEvent. The alt tag ("Payment targets") is now included
in both create() and updatePaymentTargets() methods.
https://claude.ai/code/session_013SQtN37Qemiu3vxjXZDbQj
Compares the 32-byte key against n byte-by-byte (big-endian) instead of
decoding into limbs first. Eliminates one IntArray(8) allocation and the
fromBytes conversion. The non-zero check ORs all bytes in a single pass.
privKeyTweakAdd was also tested with a byte-based approach but reverted
because 32 byte-iterations are slower than 8 limb-iterations — the JVM
optimizes Long operations better than byte loops.
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
For uncompressed keys (04 || x || y), compression only needs the y parity
bit and a copy of x. The previous implementation decoded both coordinates
into field limbs, validated y²=x³+7 with 2 field muls, then re-encoded.
The new implementation reads the last byte of y (parity bit), copies the
32 x-bytes, and sets the 02/03 prefix. No IntArray allocations, no field
arithmetic, no curve validation — just byte manipulation.
For already-compressed input, returns the input unchanged.
Benchmark: pubKeyCompress 658K → 6.7M ops/s (was 4.5x slower, now 2.4x faster than native)
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
Fills coverage gaps identified by audit, especially for areas where
bugs were found during development:
GlvTest (4 → 14 tests):
- wNAF reconstruction for small, large, and high-bit scalars
- wNAF carry overflow at bit 255 (regression test for the fixed bug)
- wNAF digits are odd and bounded, zero-run guarantee verified
- splitScalar with zero, n-1, and 5 different scalar values
- splitScalar halves are ~128 bits (upper limbs zero)
- β³ ≡ 1 (mod p) verification
- mulDoubleG with zero e scalar
FieldPTest (22 → 27 tests):
- half(p-1), inv(2), sqrt(0), sqrt(1)
- mul aliasing (output == input)
PointTest (22 → 25 tests):
- addMixed with equal points (should double)
- addMixed with inverse points (should give infinity)
- parsePublicKey with compressed odd-y key round-trip
Secp256k1Test (14 → 17 tests):
- verifySchnorr with wrong message (negative test)
- verifySchnorr with corrupted signature (negative test)
- signSchnorr deterministic (null auxrand produces same signature)
Total: 126 → 146 tests
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
Splits the 914-line Point.kt into two focused files:
- Glv.kt (248 lines): GLV endomorphism constants, scalar decomposition
(splitScalar), Babai rounding (mulShift384), and wNAF encoding. This is
a self-contained algorithm that only operates on scalars (no EC points).
- Point.kt (683 lines): EC point types, core operations (double, addMixed,
addPoints), scalar multiplication (mul, mulG, mulDoubleG), coordinate
conversion (toAffine, liftX), and key serialization.
Each file has a comprehensive header explaining its purpose and the
algorithms it implements. The Point.kt header is updated to reflect the
current state (GLV and wNAF are implemented, not "future optimizations").
mulDoubleG now references Glv.splitScalar and Glv.wnaf instead of local
methods. GlvTest updated to use the Glv object directly.
No functional changes — pure file reorganization with updated documentation.
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
- Replace 3-way ByteArray concatenation for tagged hash inputs with
single pre-sized array + copyInto calls (avoids 3 intermediate arrays)
- Add U256.fromBytes(bytes, offset) overload to decode from a slice
without copyOfRange allocation
- Build signature output using toBytesInto instead of concatenation
- Apply same pattern to signSchnorr's nonce and challenge hash inputs
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
Karatsuba multiplication (splitting 8 limbs into 4+4 halves for 48 inner
products instead of 64) was implemented and tested but reverted because
the overhead of extra additions, carry propagation, and 5 temporary array
allocations per call negates the product-count savings at only 8 limbs.
The crossover point where Karatsuba beats schoolbook is typically ~32+
limbs on hardware with fast multiply.
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
Three targeted optimizations in the verify hot path:
1. Precompute and cache lambda(G) table: the 8 AffinePoints for λ(G)
odd-multiples are now lazily initialized once (like gTable) instead
of recomputing 8 field multiplications per verify call.
2. Efficient P odd-multiples table: build [1P, 3P, 5P, ..., 15P] via
1 doubling + 7 additions (compute 2P then add repeatedly) instead
of building all 16 multiples [1P..16P] with 15 additions and
discarding the even ones.
3. Pre-allocated Jacobian negation scratch: the MutablePoint used for
negating P-side table entries (when wNAF digit is negative) is now
allocated once before the main loop instead of per-digit.
Also removed the unused maybeNegateTable function.
Benchmark: verifySchnorr 3,429 → 3,556 ops/s (7.2x vs native)
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
Replaces generic square-and-multiply exponentiation in inv() and sqrt()
with hand-crafted addition chains derived from libsecp256k1.
The key insight: p-2 and (p+1)/4 have long runs of 1-bits (since p ≈ 2^256),
so generic powModP wastes ~230+ multiplications on redundant mul-by-base steps.
The addition chain instead builds a^(2^k - 1) for k = 2,3,6,9,11,22,44,88,176,
220,223 via a ladder of squarings, then combines them with a short tail.
Savings per call:
inv: 255 sqr + 15 mul = 270 ops (was 255 sqr + 248 mul = 503 ops) → -233 muls
sqrt: 254 sqr + 13 mul = 267 ops (was 253 sqr + 246 mul = 499 ops) → -233 muls
Each verify does one inv (toAffine) + one sqrt (liftX), saving ~466 field
multiplications = ~29,800 fewer inner products per verification.
Also removes the now-unused generic powModP function and its P_MINUS_2 /
P_PLUS_1_DIV_4 exponent constants.
Benchmark: verifySchnorr 3,254 → 3,429 ops/s (~5% faster, 8.2x vs native)
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
Implements the secp256k1 GLV (Gallant-Lambert-Vanstone) endomorphism to
halve the number of point doublings during signature verification.
How it works: secp256k1 has an efficiently computable endomorphism
φ(x,y) = (β·x, y) where β is a cube root of unity in the field.
The corresponding scalar λ satisfies λ·P = φ(P). Any 256-bit scalar k
can be decomposed into k = k₁ + k₂·λ (mod n) where k₁, k₂ are ~128 bits.
This means k·P = k₁·P + k₂·(β·P.x, P.y), requiring only ~130 doublings
instead of 256.
For verification (s·G - e·P), both scalars are split into halves,
giving 4 streams processed in a single pass: s₁·G, s₂·λ(G), e₁·P, e₂·λ(P).
Key fixes from earlier debugging:
- MINUS_LAMBDA constant was wrong (byte-level transcription error)
- G1/G2 Babai rounding constants were truncated to ~142 bits instead of
the full 256-bit values from libsecp256k1
- wNAF overflow fix: extended working array with maxOf(totalBits, scalar.size)
to handle scalars larger than maxBits (IntArray(8) > IntArray(5) for 129-bit)
- GLV sign handling: XOR the negation flag with each wNAF digit sign instead
of pre-baking into tables (avoids double-negation on negative digits)
- P-side uses Jacobian tables (avoids 8 expensive field inversions that
would negate the GLV speedup)
Tests: 4 new GLV-specific tests (scalar split reconstruction, endomorphism
correctness, wNAF+GLV k1*G, mulDoubleG with zero scalar)
Benchmark improvement for verifySchnorr:
Before (wNAF only): 2,626 ops/s (10.6x vs native)
After (wNAF + GLV): 3,254 ops/s (8.7x vs native)
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
Fixed the wNAF encoding bug and wired it into the verification hot path.
Bug fix: The wNAF encoding silently dropped carry bits that overflowed past
the 256-bit scalar boundary. When a negative digit near bit 251 caused a
carry to bit 256, the extraction window was clamped to 0 bits by
`w.coerceAtMost(maxBits - bit)`, causing the carry digit to be lost.
Fixed by extending the working copy and result arrays to accommodate
carries up to bit (maxBits + w), and using totalBits instead of maxBits
for the window size clamp.
Performance: wNAF-5 (windowed Non-Adjacent Form, width 5) encodes scalars
using signed odd digits {±1, ±3, ..., ±15} with guaranteed ≥4 zero-runs
between non-zero digits. This reduces point additions in mulDoubleG from
~120 (4-bit window) to ~86 for two 256-bit scalars, a ~28% reduction in
additions while the 256 doublings remain the same.
Benchmark improvement for verifySchnorr:
Before: 1,940 ops/s (12.9x vs native)
After: 2,626 ops/s (10.6x vs native)
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
Each object now lives in its own file for easier navigation and review:
- U256.kt (284 lines): Raw 256-bit unsigned integer arithmetic with the
file-level architecture documentation explaining representation choices
- FieldP.kt (360 lines): Field arithmetic modulo the secp256k1 prime p,
including reduction, inversion, and square root
- ScalarN.kt (230 lines): Scalar arithmetic modulo the group order n,
including wide reduction and Fermat inversion
No functional changes — pure file reorganization.
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
Major cleanup of the pure-Kotlin secp256k1 implementation for readability:
Documentation:
- Added file-level architecture comments explaining representation choices
(why 8×32-bit limbs, why not 5×52-bit like C), field reduction strategy,
and performance approach (mutable output params, thread-local scratch)
- Added single-paragraph explainers for domain jargon: Jacobian coordinates,
Fermat inversion vs safegcd, windowed scalar multiplication, Shamir's trick,
GLV endomorphism, wNAF encoding
- Documented every public function with purpose, cost, and usage context
- Added inline comments explaining the math in point doubling/addition formulas
Removed dead code (-400 lines):
- straussGlvGP: GLV-accelerated Strauss method (had sign-handling bug)
- scalarSplitLambda, SplitResult, isHigh: GLV scalar decomposition
- wnaf, getBitsVar, addBitTo: wNAF encoding functions
- mulLambdaAffine, addMixedWithSign, buildOddMultiplesTable: GLV support
- All GLV constants (BETA, LAMBDA, MINUS_LAMBDA, G1, G2, MINUS_B1, MINUS_B2)
- U256.mulShift: used only by GLV scalar decomposition
These are preserved in git history and can be restored once the wNAF
interaction bug with the verify path is understood and fixed.
Structure:
- Field.kt: Clear sections (U256 → FieldP → ScalarN) with headers
- Point.kt: Sections (types → doubling → mixed add → full add → scalar mul
→ conversion → serialization) with formula documentation
- Secp256k1.kt: Grouped by purpose (keys → BIP-340 → tweaks) with
algorithm steps documented in KDoc
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
Phase 1 optimizations from C code analysis:
1. Dedicated sqrWide: Exploits a[i]*a[j] symmetry — 36 inner products
instead of 64. Reduces field squaring cost by ~40%.
2. Mixed Jacobian+Affine addition (addMixed): 8M+3S instead of 12M+4S.
Saves 4 multiplications per addition when one operand is affine.
Used for precomputed G table lookups during Shamir's trick.
3. Optimized point doubling (3M+4S via fe_half): Uses the (3/2)*X²
formula from libsecp256k1, replacing a field multiplication with a
cheap halving operation (carry-propagating right shift).
4. fe_half: Branchless divide-by-2 mod p, used by the new doubling formula.
5. AffinePoint type: Stores precomputed table entries as (x,y) without z,
enabling mixed addition. G table now stored as affine.
6. U256.mulShift: 256x256→shift multiplication for future GLV scalar
decomposition.
7. GLV infrastructure (straussGlvGP, scalarSplitLambda, wNAF, endomorphism
constants): Implemented but not yet wired into the verify hot path due
to sign-handling bugs being debugged. The 4-stream Strauss with GLV
will halve doublings from 256→128 once the sign logic is fixed.
Benchmark: verifySchnorr 1,940 → 2,116 ops/s (~9% improvement)
The modest gain reflects that only G-side additions use mixed add;
P-side still uses full Jacobian. GLV will provide the next big jump.
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
Key optimizations:
1. Mutable field operations: All FieldP hot-path methods (add, sub, mul, sqr)
now write into caller-provided output arrays instead of allocating new ones.
Thread-local IntArray(16) scratch for mulWide avoids per-mul allocation.
2. Mutable point operations: MutablePoint replaces immutable JPoint. Point
doubling/addition write into output points. Aliasing protection via
thread-local copy buffer for in-place doublePoint(out, out).
3. 4-bit windowed scalar multiplication: Processes 4 bits per iteration
(16 table entries) instead of 1 bit. Reduces point additions by ~4x.
4. Precomputed G table: Static lazy table of 16*G multiples. Generator
multiplication (signing, key creation) uses precomputed table directly.
5. Shamir's trick (mulDoubleG): Computes s*G + e*P in a single pass for
verification, eliminating the need for two separate scalar multiplications.
This roughly halves the cost of verifySchnorr.
6. Cached BIP-340 tag hashes: SHA256("BIP0340/challenge") etc. computed
once and reused, eliminating 2 SHA256 calls per verify.
7. toBytesInto: Writes directly into existing ByteArray at offset,
avoiding intermediate allocations in serialization.
https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg