55#include " modexp.hpp"
66#include < evmmax/evmmax.hpp>
77#include < bit>
8- #include < memory >
8+ #include < memory_resource >
99#include < ranges>
1010
1111using 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().
376378void 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