Skip to content

Commit 33a61b7

Browse files
authored
crypto: Optimize scratch space in modexp_odd (#1469)
Overlap t and rem_scratch buffers in modexp_odd which have exclusive lifetimes: rem_scratch is only used during the rem() call, t is only used in the AMM loop after rem() returns. Also simplify mask_pow2 to a branchless expression. Reduces modexp_odd scratch from 5n+3b+2 to 4n+3b+2 words. Stack buffer capacity drops from 4b+8m+4 to 4b+7m+4 (11 KB for 1024-byte inputs, down from 12 KB). Depends on #1467.
1 parent e404a1c commit 33a61b7

2 files changed

Lines changed: 18 additions & 15 deletions

File tree

lib/evmone_precompiles/modexp.cpp

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,7 @@ void mul_amm(std::span<uint64_t> r, std::span<const uint64_t> y, std::span<const
374374
}
375375

376376
/// 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().
377+
/// Scratch space required: 4n + 3*base.size() + 2 words, where n = mod.size().
378378
void modexp_odd(std::span<uint64_t> result, std::span<const uint64_t> base, Exponent exp,
379379
std::span<const uint64_t> mod, std::span<uint64_t> scratch) noexcept
380380
{
@@ -386,17 +386,16 @@ void modexp_odd(std::span<uint64_t> result, std::span<const uint64_t> base, Expo
386386
const auto n = mod.size();
387387
const auto mod_inv = -evmmax::modinv(mod[0]);
388388

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);
389+
// Layout: u[n+base.size()] | base_mont[n] | t/rem_scratch[max(n, 2*(n+base.size())+2)]
390+
// t and rem_scratch share the same region (exclusive lifetimes).
391+
assert(scratch.size() >= 4 * n + 3 * base.size() + 2);
392392

393393
// Compute base_mont = (base * R) % mod, where R = 2^(n*64).
394394
// The numerator u = base << (n*64): base in the upper words, lower n words are zero.
395395
const auto u = scratch.subspan(0, n + base.size());
396396
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());
397+
const auto t = scratch.subspan(2 * n + base.size(), n);
398+
const auto rem_scratch = scratch.subspan(2 * n + base.size(), 2 * n + 2 * base.size() + 2);
400399

401400
std::ranges::fill(u.first(n), uint64_t{0}); // Lower n words of u must be zero.
402401
std::ranges::copy(base, u.subspan(n).begin());
@@ -429,14 +428,13 @@ void modexp_odd(std::span<uint64_t> result, std::span<const uint64_t> base, Expo
429428
}
430429

431430
/// Trims the multi-word number x[] to k bits.
432-
/// TODO: Currently this assumes no leading zeros in x. Re-design this after modexp is dynamic.
433431
void mask_pow2(std::span<uint64_t> x, unsigned k) noexcept
434432
{
435433
assert(k != 0);
436-
assert(x.size() >= (k + 63) / 64);
437-
assert(!x.empty());
438-
if (const auto rem = k % 64; rem != 0)
439-
x.back() &= (uint64_t{1} << rem) - 1;
434+
assert(x.size() == (k + 63) / 64);
435+
// This implementation assumes the x.size() matches the k so we always mask the top word.
436+
// For k % 64 == 0, we don't mask anything.
437+
x.back() &= ~uint64_t{0} >> (-k % 64);
440438
}
441439

442440
/// Computes r[] = base[]^exp % 2^k.
@@ -527,10 +525,10 @@ void modexp(std::span<const uint8_t> base_bytes, std::span<const uint8_t> exp_by
527525

528526
// Bump allocator for all working memory (values + scratch).
529527
// 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.
528+
// Capacity: values[b+2m] + op scratch[4m+3b+2] + CRT[m+2] = 4b+7m+4 words.
531529
// The worst case is an even modulus with 1 trailing zero bit (odd_size=m, pow2_size=1).
532530
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;
531+
static constexpr size_t STACK_CAPACITY = 4 * MAX_SIZE + 7 * MAX_SIZE + 4;
534532
alignas(uint64_t) std::byte stack_buf[STACK_CAPACITY * sizeof(uint64_t)];
535533
std::pmr::monotonic_buffer_resource pool{stack_buf, sizeof(stack_buf)};
536534
std::pmr::polymorphic_allocator<uint64_t> alloc{&pool};
@@ -571,7 +569,7 @@ void modexp(std::span<const uint8_t> base_bytes, std::span<const uint8_t> exp_by
571569
const auto need_crt = !pow2_is_trivial && !odd_is_trivial;
572570

573571
// 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;
572+
const size_t odd_scratch = !odd_is_trivial ? 4 * odd_size + 3 * base.size() + 2 : 0;
575573
const size_t pow2_scratch = !pow2_is_trivial ? pow2_size : 0;
576574
const size_t inv_scratch = need_crt ? 2 * pow2_size : 0;
577575
const size_t op_scratch_size = std::max({odd_scratch, pow2_scratch, inv_scratch});

test/unittests/precompiles_expmod_test.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,11 @@ TEST_P(expmod, large_inputs)
315315
EXPECT_EQ(run(make_val(1025, 0x40, 0x03), {0x01}, make_val(1025, 0x80, 2)),
316316
make_val(1025, 0x40, 0x03));
317317

318+
// Even modulus with tiny odd part and large pow2 factor.
319+
// mod = 3 * 256^1023 (1024 bytes). inv_scratch dominates op_scratch.
320+
// 2^1 mod M = 2.
321+
expect_last_byte(run({0x02}, {0x01}, make_val(1024, 0x03)), 2);
322+
318323
// Dense values: (2^N - 2)^2 mod (2^N - 1) = 1. Tests AMM reduction at various sizes.
319324
for (const auto n : {size_t{64}, size_t{128}, size_t{256}, size_t{512}, size_t{1024}})
320325
expect_last_byte(

0 commit comments

Comments
 (0)