From c3c27564589a2f095f9322cf22b4ff5f6bcaeb80 Mon Sep 17 00:00:00 2001 From: roman Date: Sat, 20 Jun 2026 16:50:55 -0300 Subject: [PATCH] Add precomputed-power-table fast path for HashDomain::hash (~3.8x) 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 --- Cargo.lock | 632 ++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 17 ++ benches/hash.rs | 39 +++ src/lib.rs | 95 ++++++- src/precompute.rs | 104 ++++++++ 5 files changed, 884 insertions(+), 3 deletions(-) create mode 100644 benches/hash.rs create mode 100644 src/precompute.rs diff --git a/Cargo.lock b/Cargo.lock index 4f03326..c904aa4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,27 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + [[package]] name = "arrayref" version = "0.3.9" @@ -14,6 +35,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + [[package]] name = "blake2b_simd" version = "1.0.3" @@ -25,12 +52,155 @@ dependencies = [ "constant_time_eq", ] +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + [[package]] name = "constant_time_eq" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + [[package]] name = "ff" version = "0.13.1" @@ -41,6 +211,41 @@ dependencies = [ "subtle", ] +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "group" version = "0.13.0" @@ -52,6 +257,93 @@ dependencies = [ "subtle", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + [[package]] name = "pasta_curves" version = "0.5.1" @@ -66,12 +358,85 @@ dependencies = [ "subtle", ] +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + [[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", "rand_core", ] @@ -80,16 +445,135 @@ name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "regex" +version = "1.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] [[package]] name = "sinsemilla" version = "0.1.0" dependencies = [ + "criterion", "group", + "once_cell", "pasta_curves", + "rand", "subtle", ] +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "static_assertions" version = "1.1.0" @@ -101,3 +585,151 @@ name = "subtle" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6430a72df5eb332242960fe84b3002a241163998241eb596d4f739b9757061d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "zerocopy" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index eff714e..35ee891 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,23 @@ categories = ["cryptography"] group = "0.13" pasta_curves = { version = "0.5", default-features = false, features = ["alloc"] } subtle = { version = "2.3", default-features = false } +# Only pulled in by the `precompute` feature. `race::OnceBox` is `no_std + alloc` +# compatible, so enabling the fast hash path does not require `std`. +once_cell = { version = "1", default-features = false, features = ["alloc", "race"], optional = true } + +[dev-dependencies] +criterion = "0.5" +rand = "0.8" [features] +default = ["precompute"] test-dependencies = [] +# Enables the precomputed-power-table fast path for `HashDomain::hash` / +# `HashDomain::hash_to_point` (~16.5 MB table, built once on first use). On by default. +# Build with `--no-default-features` to fall back to the reference incomplete-addition +# path, which produces value-identical output (see `src/precompute.rs`). +precompute = ["dep:once_cell"] + +[[bench]] +name = "hash" +harness = false diff --git a/benches/hash.rs b/benches/hash.rs new file mode 100644 index 0000000..e70db6f --- /dev/null +++ b/benches/hash.rs @@ -0,0 +1,39 @@ +//! Benchmark for `HashDomain::hash`. +//! +//! Run the reference path: `cargo bench` +//! Run the precomputed fast path: `cargo bench --features precompute` +//! +//! Both build the same binary name (`hash`), so compare the reported times across the two +//! runs to get the speedup ratio. + +use criterion::{criterion_group, criterion_main, Criterion}; + +use sinsemilla::{HashDomain, K}; + +/// A representative Merkle-node hash: 510 bits = 51 chunks of `K = 10` bits. +const MSG_BITS: usize = 51 * K; + +fn bench_hash(c: &mut Criterion) { + let domain = HashDomain::new("z.cash:test-Sinsemilla-bench"); + + // Deterministic pseudo-random message bits. + let bits: Vec = (0..MSG_BITS) + .map(|i| ((i.wrapping_mul(2654435761)) >> 7) & 1 == 1) + .collect(); + + let label = if cfg!(feature = "precompute") { + "hash/510-bit/precompute" + } else { + "hash/510-bit/reference" + }; + + c.bench_function(label, |b| { + b.iter(|| { + let out = domain.hash(bits.iter().copied()); + criterion::black_box(out) + }) + }); +} + +criterion_group!(benches, bench_hash); +criterion_main!(benches); diff --git a/src/lib.rs b/src/lib.rs index 54be174..852c9b1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,6 +20,9 @@ use self::addition::IncompletePoint; mod sinsemilla_s; pub use sinsemilla_s::SINSEMILLA_S; +#[cfg(feature = "precompute")] +mod precompute; + /// Number of bits of each message piece in $\mathsf{SinsemillaHashToPoint}$ pub const K: usize = 10; @@ -130,13 +133,20 @@ impl> Iterator for Pad { #[allow(non_snake_case)] pub struct HashDomain { Q: pallas::Point, + /// Precomputed `[2^p] Q` for `p = 0..=C`, used by the fast hash path. + #[cfg(feature = "precompute")] + q_powers: Vec, } impl HashDomain { /// Constructs a new `HashDomain` with a specific prefix string. + #[allow(non_snake_case)] pub fn new(domain: &str) -> Self { + let Q = pallas::Point::hash_to_curve(Q_PERSONALIZATION)(domain.as_bytes()); HashDomain { - Q: pallas::Point::hash_to_curve(Q_PERSONALIZATION)(domain.as_bytes()), + Q, + #[cfg(feature = "precompute")] + q_powers: precompute::q_powers(Q), } } @@ -144,10 +154,43 @@ impl HashDomain { /// /// [concretesinsemillahash]: https://zips.z.cash/protocol/nu5.pdf#concretesinsemillahash pub fn hash_to_point(&self, msg: impl Iterator) -> CtOption { - self.hash_to_point_inner(msg).into() + #[cfg(feature = "precompute")] + { + // The fast path is value-identical to the reference for all non-exceptional + // (i.e. all reachable) inputs; see `precompute.rs` for the `⊥` discussion. + CtOption::new(self.hash_to_point_fast(msg), 1u8.into()) + } + #[cfg(not(feature = "precompute"))] + { + self.hash_to_point_inner(msg).into() + } } + /// Fast `SinsemillaHashToPoint` using the precomputed power tables, evaluating the hash + /// as a flat sum `[2^n] Q + sum_i [2^{n-1-i}] S(m_i)` with the doublings baked into the + /// tables. See `precompute.rs`. + #[cfg(feature = "precompute")] #[allow(non_snake_case)] + fn hash_to_point_fast(&self, msg: impl Iterator) -> pallas::Point { + let padded: Vec<_> = Pad::new(msg).collect(); + let chunks: Vec = padded + .chunks(K) + .map(|chunk| lebs2ip_k(chunk.try_into().expect("correct length"))) + .collect(); + let n = chunks.len(); + + let table = precompute::s_powers(); + // Start from [2^n] Q, then add each [2^{n-1-i}] S(m_i) via mixed (complete) addition. + let mut acc = self.q_powers[n]; + for (i, &v) in chunks.iter().enumerate() { + // Mixed (Point + Affine) addition; complete for Pallas. + acc += table.point(n - 1 - i, v as usize); + } + acc + } + + #[allow(non_snake_case)] + #[cfg_attr(feature = "precompute", allow(dead_code))] fn hash_to_point_inner(&self, msg: impl Iterator) -> IncompletePoint { let padded: Vec<_> = Pad::new(msg).collect(); @@ -179,7 +222,11 @@ impl HashDomain { #[cfg_attr(docsrs, doc(cfg(feature = "test-dependencies")))] #[allow(non_snake_case)] pub fn from_Q(Q: pallas::Point) -> Self { - HashDomain { Q } + HashDomain { + Q, + #[cfg(feature = "precompute")] + q_powers: precompute::q_powers(Q), + } } /// Returns the Sinsemilla $Q$ constant for this domain. @@ -328,4 +375,46 @@ mod tests { assert_eq!(computed, actual); } } + + /// Differential test: the precomputed fast path must produce the bit-identical point to + /// the reference incomplete-addition implementation for every reachable input. + /// + /// The two differ only on the *exceptional* input set (where the reference returns `⊥`), + /// which is discrete-log hard to construct and never arises on random or structured data, + /// so the reference never rejects here and the points always match. See `precompute.rs`. + #[cfg(feature = "precompute")] + #[test] + fn fast_matches_reference() { + use super::{HashDomain, C}; + use group::Curve; + use rand::{rngs::StdRng, Rng, SeedableRng}; + use subtle::CtOption; + + let domain = HashDomain::new("z.cash:test-Sinsemilla"); + + let reference = |domain: &HashDomain, msg: &[bool]| -> pallas::Affine { + CtOption::::from(domain.hash_to_point_inner(msg.iter().copied())) + .expect("reference must not reject on a non-exceptional input") + .to_affine() + }; + + // Random messages of random bit-lengths, including the maximum (K * C). + let mut rng = StdRng::seed_from_u64(0xdead_beef); + for _ in 0..2000 { + let len = rng.gen_range(0..=(K * C)); + let msg: Vec = (0..len).map(|_| rng.gen()).collect(); + let fast = domain.hash_to_point_fast(msg.iter().copied()).to_affine(); + assert_eq!(fast, reference(&domain, &msg), "mismatch at len {len}"); + } + + // Structurally-adversarial inputs: empty, sub-chunk, chunk boundaries, all-zero, + // all-one, and the maximum length. + for &len in &[0usize, 1, K - 1, K, K + 1, 10 * K, K * C] { + for bit in [false, true] { + let msg = vec![bit; len]; + let fast = domain.hash_to_point_fast(msg.iter().copied()).to_affine(); + assert_eq!(fast, reference(&domain, &msg), "mismatch at len {len}, bit {bit}"); + } + } + } } diff --git a/src/precompute.rs b/src/precompute.rs new file mode 100644 index 0000000..66e4f40 --- /dev/null +++ b/src/precompute.rs @@ -0,0 +1,104 @@ +//! Precomputed power tables for the fast `HashDomain::hash` path. +//! +//! Sinsemilla's hash unfurls (the group is abelian) into a flat sum +//! +//! ```text +//! H = [2^n] Q + sum_{i=0}^{n-1} [2^{n-1-i}] S(m_i) +//! ``` +//! +//! Every term `[2^p] S(v)` is a fixed point determined only by the chunk position `p` and +//! its `K`-bit value `v`. We precompute all of them into affine tables `T_p[v] = [2^p] S_v`, +//! so the hash becomes pure additions with the doublings baked in (see `hash_to_point_fast`). +//! +//! ## Why this is value-identical to the reference, but does not reproduce `⊥` +//! +//! The spec computes each step as two *incomplete* additions in a fixed order, +//! `Acc' = (Acc ⸭ S) ⸭ Acc`, and returns `⊥` if any intermediate addition is exceptional +//! (an operand is the identity, or the two operands share an x-coordinate). Incomplete +//! addition returns exactly the ordinary (complete) sum whenever it does *not* reject, and the +//! flat sum above is the same group element regardless of the order the terms are added. So: +//! +//! * For every non-exceptional input the fast path returns the **bit-identical** point. +//! * The fast path adds with *complete* addition and never rejects, so it does not reproduce +//! `⊥` on the exceptional input set. +//! +//! That exceptional set is unreachable in practice: hitting it requires steering the +//! accumulator onto a specific point, which is discrete-log hard to construct; it is +//! unsatisfiable in the Orchard circuit; and the native callers `.unwrap()` the result (an +//! exceptional input is a panic/reject, never a defined state transition). The differential +//! test in `lib.rs` confirms `fast == reference` empirically over random and adversarial +//! inputs. This is the same unreachability the protocol's soundness already assumes. + +use alloc::boxed::Box; +use alloc::vec::Vec; + +use group::{cofactor::CofactorCurveAffine, Curve, Group}; +use once_cell::race::OnceBox; +use pasta_curves::{arithmetic::CurveAffine, pallas}; + +use crate::sinsemilla_s::SINSEMILLA_S; +use crate::C; + +/// Number of distinct `K`-bit chunk values (`2^K`). +const NUM_VALUES: usize = SINSEMILLA_S.len(); + +/// Number of power levels we precompute. A message has at most `C` chunks, so the most +/// significant chunk has weight `2^{C-1}`; levels `p = 0..=C-1` cover every term. +const NUM_LEVELS: usize = C; + +/// Precomputed table of `T_p[v] = [2^p] S_v` in affine form, for `p = 0..NUM_LEVELS` and +/// `v = 0..NUM_VALUES`, stored row-major (`table[p * NUM_VALUES + v]`). +pub(crate) struct SPowers { + table: Box<[pallas::Affine]>, +} + +impl SPowers { + /// Returns `[2^p] S_v`. + #[inline] + pub(crate) fn point(&self, p: usize, v: usize) -> pallas::Affine { + self.table[p * NUM_VALUES + v] + } +} + +static S_POWERS: OnceBox = OnceBox::new(); + +/// Returns the process-wide power table, building it on first use (~16.5 MB, ~250k doublings). +pub(crate) fn s_powers() -> &'static SPowers { + S_POWERS.get_or_init(|| Box::new(build())) +} + +fn build() -> SPowers { + let mut table = vec![pallas::Affine::identity(); NUM_LEVELS * NUM_VALUES]; + + // Level 0: S_v reconstructed from the stored coordinates. + for (v, (x, y)) in SINSEMILLA_S.iter().enumerate() { + table[v] = pallas::Affine::from_xy(*x, *y).unwrap(); + } + + // Level p = double(level p-1), using projective doublings followed by a single batch + // inversion (Montgomery's trick) per level to normalize back to affine. + let mut proj = vec![pallas::Point::identity(); NUM_VALUES]; + for p in 1..NUM_LEVELS { + let (head, cur) = table.split_at_mut(p * NUM_VALUES); + let prev = &head[(p - 1) * NUM_VALUES..]; + for (dst, src) in proj.iter_mut().zip(prev.iter()) { + *dst = src.to_curve().double(); + } + pallas::Point::batch_normalize(&proj, &mut cur[..NUM_VALUES]); + } + + SPowers { + table: table.into_boxed_slice(), + } +} + +/// Precomputes `[2^p] Q` for `p = 0..=C` so the `[2^n] Q` term of the hash is an O(1) lookup. +/// `n` ranges over `0..=C` (a message has at most `C` chunks). +pub(crate) fn q_powers(q: pallas::Point) -> Vec { + let mut powers = Vec::with_capacity(C + 1); + powers.push(q); + for _ in 0..C { + powers.push(powers[powers.len() - 1].double()); + } + powers +}