Precomputed-power-table fast path for HashDomain::hash (~3.8x)#1
Conversation
Sinsemilla's hash unfurls (the group is abelian) into a flat sum
H = [2^n] Q + sum_i [2^{n-1-i}] S(m_i)
where each term [2^p] S(v) is a fixed point. Precomputing them into affine
tables T_p[v] = [2^p] S_v turns the hash into pure mixed additions with the
doublings baked in, eliminating the per-chunk doubling + full projective add
+ constant-time bottom checks of the reference recurrence.
Measured on a 510-bit (51-chunk) Merkle input: 68.85us -> 18.01us (~3.8x).
- New `precompute` feature (on by default), backed by once_cell::race::OnceBox
so the fast path stays no_std + alloc. `--no-default-features` falls back to
the unchanged reference path, which is value-identical.
- Only `HashDomain::hash`/`hash_to_point` use the fast path. `CommitDomain`
(note commitment, commit_ivk) keeps calling hash_to_point_inner, preserving
exact incomplete-addition (bottom) semantics on every commitment path.
- Differential test asserts fast == reference over 2000 random + adversarial
inputs; criterion bench added under benches/hash.rs.
The fast path is value-identical to the reference for all reachable inputs;
it diverges only on the exceptional set where the reference returns bottom,
which is discrete-log hard to construct and unsatisfiable in the Orchard
circuit. See src/precompute.rs for the full rationale.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
|
Warning Review the following alerts detected in dependencies. According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.
|
Summary
Speeds up the CPU Sinsemilla hash (
HashDomain::hash/hash_to_point) by ~3.8x using precomputed fixed-base power tables — the Sinsemilla analogue of the Pedersen fixed-base windowing trick.The hash unfurls (the group is abelian) into a flat sum:
Each term
[2^p] S(v)is a fixed point determined only by the chunk positionpand itsK-bit valuev. We precompute all of them into affine tablesT_p[v] = [2^p] S_v, so the hash becomes pure mixed additions with all the doublings baked in. This drops the reference recurrence's per-chunk cost (a mixed add plus a full projective add plus constant-time⊥checks) down to a single mixed add.Benchmark
HashDomain::hashon a 510-bit / 51-chunk Merkle input (benches/hash.rs):What's in scope
precomputefeature, on by default, backed byonce_cell::race::OnceBoxso the fast path staysno_std + alloc. Table is ~16.5 MB, built once on first use (253 power levels × 1024 values, via doubling + per-level batch inversion).--no-default-featuresfalls back to the unchanged reference path.HashDomain::hash/hash_to_pointuse the fast path.CommitDomain(note commitment,commit_ivk) is unchanged — it keeps callinghash_to_point_inner, preserving exact incomplete-addition (⊥) semantics on every commitment path.hash_to_point_inner/addition.rsremain in-tree as the reference oracle.Correctness & consensus safety
The fast path adds with complete addition in a reordered sum. It is value-identical to the reference for every reachable input, because incomplete addition equals complete addition whenever it does not reject, and the flat sum is the same group element regardless of add order. It diverges from the reference only on the exceptional input set (where the reference returns
⊥), which is:Differential test (
fast_matches_reference) assertsfast == referenceover 2000 random inputs of random lengths plus structurally-adversarial inputs (empty, sub-chunk, chunk boundaries, all-zero, all-one, max length). All pass.Call-site audit (orchard consensus)
The only changed entry points (
hash/hash_to_point) are consumed by orchard in exactly one consensus spot —MerkleCRH(tree.rs), which doesdomain.hash(...).unwrap_or(zero). All⊥-sensitive paths (ivk derivation, note commitment) go throughCommitDomainand are unaffected.Note on rollout
This PR is the
sinsemillacrate only. Wiring it into downstream (orchardcurrently pinssinsemilla = "0.1"; needs a patch/bump to pick up this fork) is a separate step.Test plan
cargo test(default, precompute on) — 3 pass incl. differential testcargo test --no-default-features(reference path, no_std) — 2 passcargo clippy/cargo clippy --no-default-features— cleancargo benchvscargo benchon a reference build — ~3.8x🤖 Generated with Claude Code