Skip to content

Commit e404a1c

Browse files
authored
crypto: Unify memory allocation for modexp execution (#1467)
Replace per-function heap allocations in modexp helpers (rem, modexp_odd, modexp_pow2, modinv_pow2) with caller-provided scratch spans. The top-level modexp() uses a pmr::monotonic_buffer_resource backed by a stack buffer for inputs up to the EIP-7823 limit (1024 bytes), with automatic heap fallback for larger inputs. Each buffer (base, mod, result, op_scratch, CRT intermediates) is allocated individually from the bump allocator — no manual pointer arithmetic. The capacity formula 4b+8m+4 is tight: verified that 1024-byte inputs fit on the stack and 1025-byte CRT inputs overflow to heap.
1 parent b9f9f51 commit e404a1c

1 file changed

Lines changed: 64 additions & 49 deletions

File tree

lib/evmone_precompiles/modexp.cpp

Lines changed: 64 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
#include "modexp.hpp"
66
#include <evmmax/evmmax.hpp>
77
#include <bit>
8-
#include <memory>
8+
#include <memory_resource>
99
#include <ranges>
1010

1111
using namespace intx;
@@ -215,26 +215,28 @@ ModLoad load_mod(std::span<uint64_t> storage, std::span<const uint8_t> data) noe
215215

216216
/// Computes r[] = u[] % d[] (remainder only).
217217
/// The d[] must be non-zero. The r[] size must be >= num significant words in d[].
218-
void rem(std::span<uint64_t> r, std::span<const uint64_t> u, std::span<const uint64_t> d) noexcept
218+
/// Scratch space required: 2 * u.size() + 2 words.
219+
void rem(std::span<uint64_t> r, std::span<const uint64_t> u, std::span<const uint64_t> d,
220+
std::span<uint64_t> scratch) noexcept
219221
{
220222
assert(!d.empty());
221223
assert(!u.empty());
222224
assert(d.back() != 0);
223225
assert(u.back() != 0);
224226
assert(r.size() >= d.size());
225227
assert(u.size() > d.size()); // Because used only for to-Montgomery conversion.
228+
assert(scratch.size() >= 2 * u.size() + 2);
229+
230+
// Layout: un[u.size()+1] | dn[d.size()] | q[u.size()+1-d.size()]
231+
auto un = scratch.subspan(0, u.size() + 1);
232+
const auto dn = scratch.subspan(u.size() + 1, d.size());
233+
const auto q_buf = scratch.subspan(u.size() + 1 + d.size(), u.size() + 1 - d.size());
226234

227-
const auto un_storage = std::make_unique_for_overwrite<uint64_t[]>(u.size() + 1);
228-
auto un = std::span{un_storage.get(), u.size() + 1};
229235
un.back() = 0; // Only the extra top word needs zeroing; the rest is set by normalization.
230236

231237
// Normalize: left-shift both u and d so that the MSB of d's top word is set.
232238
const auto shift = static_cast<unsigned>(std::countl_zero(d.back()));
233239

234-
// Allocate normalized divisor.
235-
const auto dn_storage = std::make_unique_for_overwrite<uint64_t[]>(d.size());
236-
const auto dn = std::span{dn_storage.get(), d.size()};
237-
238240
if (shift != 0)
239241
{
240242
for (size_t i = d.size() - 1; i != 0; --i)
@@ -278,11 +280,9 @@ void rem(std::span<uint64_t> r, std::span<const uint64_t> u, std::span<const uin
278280
}
279281
else
280282
{
281-
// General case: Knuth's algorithm. The quotient is stored in the temporary q_storage
282-
// buffer; we don't use it, but udivrem_knuth requires storage for it.
283-
const auto q_len = un.size() - dn.size();
284-
const auto q_storage = std::make_unique_for_overwrite<uint64_t[]>(q_len);
285-
intx::internal::udivrem_knuth(q_storage.get(), un, dn);
283+
// General case: Knuth's algorithm. The quotient is stored in q_buf;
284+
// we don't use it, but udivrem_knuth requires storage for it.
285+
intx::internal::udivrem_knuth(q_buf.data(), un, dn);
286286
denormalize(un.subspan(0, dn.size()));
287287
}
288288
}
@@ -373,8 +373,10 @@ void mul_amm(std::span<uint64_t> r, std::span<const uint64_t> y, std::span<const
373373
std::ranges::copy(t, r.begin());
374374
}
375375

376+
/// Computes result[] = base[]^exp % mod[] for odd mod[] (mod[0] % 2 != 0).
377+
/// Scratch space required: 5n + 3*base.size() + 2 words, where n = mod.size().
376378
void modexp_odd(std::span<uint64_t> result, std::span<const uint64_t> base, Exponent exp,
377-
std::span<const uint64_t> mod) noexcept
379+
std::span<const uint64_t> mod, std::span<uint64_t> scratch) noexcept
378380
{
379381
assert(!mod.empty() && mod.back() != 0); // mod must be trimmed.
380382
assert(!base.empty() && base.back() != 0); // base must be trimmed.
@@ -384,16 +386,21 @@ void modexp_odd(std::span<uint64_t> result, std::span<const uint64_t> base, Expo
384386
const auto n = mod.size();
385387
const auto mod_inv = -evmmax::modinv(mod[0]);
386388

389+
// Layout: u[n+base.size()] | base_mont[n] | t[n] | rem_scratch[2*(n+base.size())+2]
390+
// After rem() returns, the rem_scratch is dead.
391+
assert(scratch.size() >= 5 * n + 3 * base.size() + 2);
392+
387393
// Compute base_mont = (base * R) % mod, where R = 2^(n*64).
388394
// The numerator u = base << (n*64): base in the upper words, lower n words are zero.
389-
const auto tmp_storage = std::make_unique_for_overwrite<uint64_t[]>(n + base.size() + n + n);
390-
const auto u = std::span{tmp_storage.get(), n + base.size()};
391-
const auto base_mont = std::span{tmp_storage.get() + n + base.size(), n};
392-
const auto t = std::span{tmp_storage.get() + n + base.size() + n, n};
395+
const auto u = scratch.subspan(0, n + base.size());
396+
const auto base_mont = scratch.subspan(n + base.size(), n);
397+
// TODO: t and rem_scratch have exclusive lifetimes.
398+
const auto t = scratch.subspan(n + base.size() + n, n);
399+
const auto rem_scratch = scratch.subspan(3 * n + base.size());
393400

394401
std::ranges::fill(u.first(n), uint64_t{0}); // Lower n words of u must be zero.
395402
std::ranges::copy(base, u.subspan(n).begin());
396-
rem(base_mont, u, mod);
403+
rem(base_mont, u, mod, rem_scratch);
397404

398405
// Reuse the lower n words of u as the result buffer r.
399406
const auto r = u.subspan(0, n);
@@ -435,7 +442,9 @@ void mask_pow2(std::span<uint64_t> x, unsigned k) noexcept
435442
/// Computes r[] = base[]^exp % 2^k.
436443
/// Only the low-order words matching the k bits of the base are used.
437444
/// Also, the same amount of the result words are produced. The rest is not modified.
438-
void modexp_pow2(std::span<uint64_t> r, std::span<const uint64_t> base, Exponent exp, unsigned k)
445+
/// Scratch space required: (k + 63) / 64 words.
446+
void modexp_pow2(std::span<uint64_t> r, std::span<const uint64_t> base, Exponent exp, unsigned k,
447+
std::span<uint64_t> scratch) noexcept
439448
{
440449
assert(k != 0); // Modulus of 1 should be covered as "odd".
441450
assert(exp.bit_width() != 0); // Exponent of zero must be handled outside.
@@ -444,16 +453,13 @@ void modexp_pow2(std::span<uint64_t> r, std::span<const uint64_t> base, Exponent
444453

445454
const size_t num_pow2_words = (k + 63) / 64;
446455
assert(r.size() >= num_pow2_words);
456+
assert(scratch.size() >= num_pow2_words);
447457

448458
auto r_k = r.subspan(0, num_pow2_words);
459+
auto tmp = scratch.subspan(0, num_pow2_words);
449460

450461
const auto base_k = base.subspan(0, std::min(base.size(), num_pow2_words));
451462

452-
// Allocate temporary storage for iterations.
453-
// TODO: Move to stack if the size is small enough or provide from the caller.
454-
const auto tmp_storage = std::make_unique_for_overwrite<uint64_t[]>(num_pow2_words);
455-
auto tmp = std::span{tmp_storage.get(), num_pow2_words};
456-
457463
const auto [_, pad] = std::ranges::copy(base_k, r_k.begin());
458464
std::ranges::fill(std::span{pad, r_k.end()}, uint64_t{0});
459465

@@ -471,32 +477,30 @@ void modexp_pow2(std::span<uint64_t> r, std::span<const uint64_t> base, Exponent
471477

472478
mask_pow2(r_k, k);
473479

474-
// r_k may point to the tmp_storage. Copy back to the result buffer if needed.
480+
// r_k may point to scratch. Copy back to the result buffer if needed.
475481
if (r_k.data() != r.data())
476482
std::ranges::copy(r_k, r.begin());
477483
}
478484

479485
/// Computes modular inversion of the multi-word number x[] modulo 2^(r.size() * 64).
480-
void modinv_pow2(std::span<uint64_t> r, std::span<const uint64_t> x) noexcept
486+
/// Scratch space required: 2 * r.size() words.
487+
void modinv_pow2(
488+
std::span<uint64_t> r, std::span<const uint64_t> x, std::span<uint64_t> scratch) noexcept
481489
{
482490
assert(!x.empty() && (x[0] & 1) != 0); // x must be odd.
483491
assert(!r.empty());
492+
assert(scratch.size() >= 2 * r.size());
484493

485494
r[0] = evmmax::modinv(x[0]); // Good start: 64 correct bits.
486495

487-
// Allocate temporary storage for iterations.
488-
// TODO: Move to stack if the size is small enough or provide from the caller.
489-
const auto tmp_storage = std::make_unique_for_overwrite<uint64_t[]>(2 * r.size());
490-
const auto tmp = std::span{tmp_storage.get(), 2 * r.size()};
491-
492496
// Each iteration doubles the number of correct bits in the inverse. See evmmax::modinv().
493497
for (size_t i = 1; i < r.size(); i *= 2)
494498
{
495499
// At the start of the iteration we have i-word correct inverse in r[0-i].
496500
// The iteration performs the Newton-Raphson step with double the precision (n=2i).
497501
const auto n = std::min(i * 2, r.size());
498-
const auto t1 = tmp.subspan(0, n);
499-
const auto t2 = tmp.subspan(n, n);
502+
const auto t1 = scratch.subspan(0, n);
503+
const auto t2 = scratch.subspan(n, n);
500504

501505
// Clamp x to available words: high words beyond x.size() are implicitly zero.
502506
mul(t1, x.subspan(0, std::min(n, x.size())), r.subspan(0, i)); // t1 = x * inv
@@ -520,14 +524,22 @@ void modexp(std::span<const uint8_t> base_bytes, std::span<const uint8_t> exp_by
520524

521525
const auto base_size = (base_bytes.size() + 7) / 8;
522526
const auto mod_size = (mod_bytes.size() + 7) / 8;
523-
const auto storage = std::make_unique_for_overwrite<uint64_t[]>(base_size + mod_size * 2);
524-
const auto base = load({storage.get(), base_size}, base_bytes);
525527

526-
// Load and decompose the modulus: mod = mod_odd * 2^mod_tz.
527-
const auto [mod_odd, mod_tz] = load_mod({storage.get() + base_size, mod_size}, mod_bytes);
528+
// Bump allocator for all working memory (values + scratch).
529+
// Stack buffer covers inputs up to the EIP-7823 limit (1024 bytes).
530+
// Capacity: values[b+2m] + op scratch[5m+3b+2] + CRT[m+2] = 4b+8m+4 words.
531+
// The worst case is an even modulus with 1 trailing zero bit (odd_size=m, pow2_size=1).
532+
static constexpr size_t MAX_SIZE = 1024 / sizeof(uint64_t); // EIP-7823
533+
static constexpr size_t STACK_CAPACITY = 4 * MAX_SIZE + 8 * MAX_SIZE + 4;
534+
alignas(uint64_t) std::byte stack_buf[STACK_CAPACITY * sizeof(uint64_t)];
535+
std::pmr::monotonic_buffer_resource pool{stack_buf, sizeof(stack_buf)};
536+
std::pmr::polymorphic_allocator<uint64_t> alloc{&pool};
537+
538+
// Allocate and load values.
539+
const auto base = load({alloc.allocate(base_size), base_size}, base_bytes);
540+
const auto [mod_odd, mod_tz] = load_mod({alloc.allocate(mod_size), mod_size}, mod_bytes);
528541
assert(!mod_odd.empty()); // Modulus of zero must be handled outside.
529-
530-
const auto result = std::span{storage.get() + base_size + mod_size, mod_size};
542+
const auto result = std::span{alloc.allocate(mod_size), mod_size};
531543
std::ranges::fill(result, uint64_t{0});
532544

533545
if (exp.bit_width() == 0) // Exponent is 0:
@@ -558,27 +570,30 @@ void modexp(std::span<const uint8_t> base_bytes, std::span<const uint8_t> exp_by
558570
// Combining results via CRT is needed when both parts are non-trivial.
559571
const auto need_crt = !pow2_is_trivial && !odd_is_trivial;
560572

561-
const auto tmp_storage_size = need_crt ? odd_size + pow2_size * 2 : 0;
562-
const auto tmp_storage = std::make_unique_for_overwrite<uint64_t[]>(tmp_storage_size);
563-
const auto tmp = std::span{tmp_storage.get(), tmp_storage_size};
573+
// Allocate operation scratch (dead after each call, reused sequentially).
574+
const size_t odd_scratch = !odd_is_trivial ? 5 * odd_size + 3 * base.size() + 2 : 0;
575+
const size_t pow2_scratch = !pow2_is_trivial ? pow2_size : 0;
576+
const size_t inv_scratch = need_crt ? 2 * pow2_size : 0;
577+
const size_t op_scratch_size = std::max({odd_scratch, pow2_scratch, inv_scratch});
578+
const auto op_scratch = std::span{alloc.allocate(op_scratch_size), op_scratch_size};
564579

565580
// Place the odd result directly in the result buffer if the CRT is not needed.
566-
const auto result_odd = need_crt ? tmp.subspan(0, odd_size) : result;
581+
const auto result_odd = need_crt ? std::span{alloc.allocate(odd_size), odd_size} : result;
567582
// Always place the power-of-two result in the result buffer.
568583
const auto result_pow2 = result.first(pow2_size);
569584

570585
if (!odd_is_trivial) [[likely]]
571-
modexp_odd(result_odd, base, exp, mod_odd); // x1 = base^exp mod mod_odd
586+
modexp_odd(result_odd, base, exp, mod_odd, op_scratch);
572587

573588
if (!pow2_is_trivial)
574-
modexp_pow2(result_pow2, base, exp, mod_tz); // x2 = base^exp mod 2^mod_tz
589+
modexp_pow2(result_pow2, base, exp, mod_tz, op_scratch);
575590

576591
if (need_crt)
577592
{
578-
const auto mod_odd_inv = tmp.subspan(odd_size, pow2_size);
579-
const auto y = tmp.subspan(odd_size + pow2_size, pow2_size);
593+
const auto mod_odd_inv = std::span{alloc.allocate(pow2_size), pow2_size};
594+
const auto y = std::span{alloc.allocate(pow2_size), pow2_size};
580595

581-
modinv_pow2(mod_odd_inv, mod_odd);
596+
modinv_pow2(mod_odd_inv, mod_odd, op_scratch);
582597
sub(result_pow2, result_odd.first(std::min(odd_size, pow2_size)));
583598
mul(y, result_pow2, mod_odd_inv);
584599
mask_pow2(y, mod_tz);

0 commit comments

Comments
 (0)