Accelerate batched trial decryption with GLV endomorphism windows - #530
Closed
LukasKorba wants to merge 1 commit into
Closed
Accelerate batched trial decryption with GLV endomorphism windows#530LukasKorba wants to merge 1 commit into
LukasKorba wants to merge 1 commit into
Conversation
Batched trial decryption multiplies every ephemeral key in a batch by each of the wallet's incoming viewing keys. This replaces the batched pipeline's per-key wNAF preparation with GLV odd-multiples windows built across the whole batch under a single shared normalization, and consumes them with a shared-doubling Straus ladder over the endomorphism split k = k1 + k2*lambda, decomposing each viewing key once per batch (via BatchDomain::batch_ka_agree_dec, new in zcash_note_encryption 0.4.2). Shared secrets are byte-identical to the per-item path, which is untouched; the GLV constants are re-verified from scratch in-crate, and the batched pipeline is tested equal to the per-item one for both the Orchard and Ironwood domains, including miss lanes and undecodable ephemeral keys. Like the existing Wnaf-based preparation, the ladder is variable-time with respect to the (wallet-local) viewing key scalar. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
LukasKorba
added a commit
to LukasKorba/pasta_curves
that referenced
this pull request
Jul 13, 2026
Both curves carry the cube-root endomorphism already exposed as CurveExt::endo(); this adds the multiplication machinery that exploits it: GLV decomposition (short lattice basis + Babai rounding), width-4 wNAF recoding, a precomputed table of odd multiples of P and phi(P), and a shared-doubling Straus ladder over the split. The API is additive and variable-time (documented): the native constant-time Mul implementations are untouched, per review guidance on zcash/orchard#530 to host this machinery at the curve layer with _glv-suffixed naming. The three cost centers are independently reusable: Table (per point, with Table::batch amortizing one field inversion across a whole batch), Decomposed (per scalar), and the ladder itself. The lattice constants are re-verified in-crate against each curve's own lambda (= Scalar::ZETA) by field arithmetic alone, and the decompose_reconstructs tests prove k1 + k2*lambda == k with both halves <= 2^127 over full-width scalars and edge cases - wrong constants cannot pass. Pallas constants match the reference implementation on zcash/orchard#530; Vesta constants are derived by the same lattice reduction (validated by reproducing the Pallas set bit-for-bit first). Benchmarks (M4, criterion): one-shot mul_glv ~34us vs ~96.5us native (2.8x); with a reused table ~26us (3.7x); batched table build 1.8us per point vs 7.7us solo. Pallas and Vesta within noise of each other. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributor
|
Subsumed by #539 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Batched trial decryption is the hot loop of light-client scanning: every compact output's ephemeral key is multiplied by each of the wallet's incoming viewing keys. This PR accelerates that loop using the Pallas cube-root endomorphism, consuming the
BatchDomain::batch_ka_agree_dechook added in zcash/zcash_note_encryption#13 and released inzcash_note_encryption 0.4.2(#14).(Strictly speaking, the GLV paper only mentioned using the Frobenius endomorphism, but the extension to cubic endomorphisms is obvious, and commonly also called GLV.)
What changes
A new private
endomodule implements GLV decomposition for the Pallas scalar field: k = k1 + k2·λ (mod r_P) with both halves ≤ 2^127, so[k] P = [k1] P + [k2] φ(P)runs as a Straus ladder with shared doublings (~128 doublings + ~52 window-4 additions instead of a ~255-doubling full-width walk). The batched pipeline amortizes everything that can be amortized:batch_epknow prepares every ephemeral key in the batch with a GLV odd-multiples window ({1,3,5,7}·P and {1,3,5,7}·φ(P), affine), normalized across the whole batch with a single shared inversion — where per-item preparation pays one inversion (or one wNAF table build) per key.batch_ka_agree_decdecomposes and wNAF-recodes the viewing key once per batch, then consumes each ephemeral key's window with the shared-doubling ladder. Each window is reused by every viewing key's multiplication against it (wallets scan with at least two ivks — external and internal scope).BatchDomainimpl is generic over the domain policy,OrchardDomainandIronwoodDomainboth get the fast path from one implementation.The per-output entry points (
try_note_decryptionetc.) are untouched:prepare_epkstill builds thegroup::Wnaftable and multiplies exactly as before. There are no public API changes; everything new ispub(crate). The minimumzcash_note_encryptionversion becomes 0.4.2 (semver-compatible).Correctness
Shared secrets are byte-identical to the per-item path, and the tests pin this from three directions:
src/endo.rs): the GLV constants are re-verified from scratch (decompose_reconstructschecks k1 + k2·λ ≡ k and the ≤ 2^127 bound over full-width scalars and edge cases — wrong constants cannot pass); the φ↔λ pairing is checked on the real curve; and the ladder is checked byte-identical to the group's own scalar multiplication over batch-built and individually-built windows, including 0, ±1, and λ.src/note_encryption.rs):batch::try_compact_note_decryptionmust produce exactly the per-item results — hits on multiple viewing keys (external + internal scope), misses, and an undecodable ephemeral key lane — for bothOrchardDomainandIronwoodDomain.batch_ka_agree_decmust produce byte-identical shared secrets to per-itemka_agree_decon hit and miss lanes alike, for both preparation routes (batch-built GLV windows and individually-prepared wNAF inputs, which take a per-item fallback arm).The full existing suite passes unchanged.
Timing
Like the existing
group::Wnaf-based preparation this complements (PreparedNonZeroScalar/PreparedNonIdentityBase), the new path is variable-time with respect to the scalar. It is used only for trial decryption with the wallet's own incoming viewing keys, so this matches the crate's existing posture for this operation.Performance
cargo bench --bench note_decryptionon an Apple M4 Pro (this branch vs. currentmain, back-to-back runs against a saved criterion baseline; medians):mainbatch-…/compact-invalid/10batch-…/compact-invalid/50batch-…/compact-invalid/100batch-…/invalid/10batch-…/invalid/50batch-…/invalid/100batch-…/valid/{10,50,100}batch-…/compact-valid/{10,50,100}The
invalidgroups are the trial-decryption miss path — what a scanning wallet does for essentially every output on chain — and improve by ~1.6×. Thevalidgroups fully decrypt every output, where key agreement is a small share of the work next to the note-commitment and esk checks. The per-item groups (note-decryption/*,compact-note-decryption/invalid) read −2.8% to +1.2% (noise band), as expected — that path is untouched.The same technique running in a production mobile sync engine (where it was developed and measured first) cut trial-decryption CPU by ~21% and end-to-end scan-call wall time by ~15% on Apple Silicon devices.