crypto: Use a sliding window in modexp - #1631
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #1631 +/- ##
==========================================
+ Coverage 97.74% 97.75% +0.01%
==========================================
Files 170 170
Lines 15439 15470 +31
Branches 3589 3592 +3
==========================================
+ Hits 15091 15123 +32
Misses 262 262
+ Partials 86 85 -1
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR updates modexp_odd to use a variable-time sliding-window exponentiation scheme with an odd-only precomputation table, reducing scratch/table size and improving performance for typical EVM (public) exponents. It also adjusts window-width selection to match GMP-style thresholds and regenerates the unit test vectors accordingly.
Changes:
- Switch
modexp_oddfrom fixed-window to sliding-window exponentiation using only odd precomputed powers. - Update window-width selection bands (now up to
w=5) and corresponding scratch/table sizing constants. - Regenerate/replace expmod unit test vectors to exercise the new window bands (
w=1..5) and the generic AMM instantiation.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| lib/evmone_precompiles/modexp.cpp | Implements sliding-window exponentiation with odd-power table; updates width thresholds and max table sizing. |
| test/unittests/precompiles_expmod_test.cpp | Updates expmod test vectors/comments to match new sliding-window bands and table behavior. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
8a49af1 to
d48b61e
Compare
7d3c278 to
0673882
Compare
The fixed window precomputes every power b^1..b^(2^w - 1). That all-powers table is the shape constant-time modexp needs, because sliding windows leak through data-dependent access. This code is not constant-time and EVM exponents are public calldata, so the table was twice the size it had to be. Store only the odd powers and align each window to a set bit, trimming trailing zeros so the value stays odd. Zero runs then cost one squaring each, and the smaller table affords one more width within the same scratch. Read each window with a single two-byte access through Exponent::window() rather than one call per exponent bit, and trim it to odd with countr_zero, which also removes the scan for the lowest set bit. A few percent fewer instructions over the benchmark matrix and over a corpus of mainnet inputs, up to about 17% on small exponents. One case regresses slightly: a modulus that is almost a power of two, where the odd part is a single word, so the table build is not amortised.
0673882 to
f3a72b5
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.
Suppressed comments (1)
lib/evmone_precompiles/modexp.cpp:396
- The stated 6-bit crossover does not describe the implemented
w=1tow=2transition. Width 1 skips precomputation, whereas width 2 performs both thebase^2multiply and thebase^3multiply, so the added cost is two multiplies and this formula yields 12 rather than 6. Please call out this special case instead of attributing the 7-bit threshold to the formula.
// Break-even points for a random exponent, where the table's extra multiply stops being
// repaid: 2^(w-1) / (1/(w+1) - 1/(w+2)) = 6, 24, 80, 240. Each narrower width is kept
// one bit longer, which measures better on the sparse small exponents seen in practice.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.
Suppressed comments (2)
lib/evmone_precompiles/modexp.cpp:389
- This increases the maximum table from 15 entries to 16, so at the 128-word EIP-7823 limit
stack_bufgrows by 128 words (1 KiB). That contradicts the PR description's claim that the extra width fits “within the same scratch”; please disclose the stack increase or revise the implementation/claim.
constexpr size_t MAX_PRECOMPUTED = size_t{1} << (MAX_WINDOW_WIDTH - 1);
lib/evmone_precompiles/modexp.cpp:397
- The 1→2 calculation is a special case: width 1 does no table work, while width 2 computes both b² and b³, so the extra cost is two AMMs and the random-exponent asymptotic break-even is 12 bits, not 6. With this cutoff,
exp = 0x80switches at 8 bits and performs the same seven loop squarings plus two unnecessary table multiplies. Please retune this first cutoff for sparse exponents (or document a measured reason for accepting that regression) and correct the rationale.
// Break-even points for a random exponent, where the table's extra multiply stops being
// repaid: 2^(w-1) / (1/(w+1) - 1/(w+2)) = 6, 24, 80, 240. Each narrower width is kept
// one bit longer, which measures better on the sparse small exponents seen in practice.
if (exp_bits <= 7)
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.
Suppressed comments (1)
lib/evmone_precompiles/modexp.cpp:389
- The PR description says the wider sliding window fits “within the same scratch,” but this increases
MAX_PRECOMPUTEDfrom 15 to 16. Consequently every odd-modulus call reserves one additional modulus word per limb, and the EIP-7823 stack buffer grows by 1 KiB at the maximum modulus size. Please either update the PR’s cost accounting to disclose this increase or adjust the width/table sizing so the previous scratch bound is preserved.
constexpr size_t MAX_PRECOMPUTED = size_t{1} << (MAX_WINDOW_WIDTH - 1);
Follow-up to #1618.
The fixed window precomputes all powers
b^1..b^(2^w-1)— the layout aconstant-time implementation needs, because sliding windows leak through
data-dependent access. This code is not constant-time and EVM exponents are public
calldata, so the table was twice the size it had to be for no return.
Now only the odd powers are stored, and each window slides onto a set bit with its
trailing zeros trimmed so the value stays odd. Zero runs then cost one squaring
each, and half the table per width buys one more width within the same scratch.
Windows are read with a single two-byte access instead of one call per bit, then
trimmed with
countr_zero.A few percent fewer instructions over the benchmark matrix and over a corpus of
unique mainnet inputs, up to about 17% on small exponents. One case regresses by
about 1%,
mod_len=32 / mod_tz=254 / exp_bits=256: the odd part of its modulus is asingle word, so the wider table is not repaid. That is a width-selection limit —
window_width()sees onlyexp_bits, not how cheap a multiply is for this modulus.