Skip to content

evmmax: Implement modexp #1135

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/evmone_precompiles/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ target_sources(
pairing/bn254/pairing.cpp
pairing/bn254/utils.hpp
pairing/field_template.hpp
modexp.hpp
modexp.cpp
ripemd160.hpp
ripemd160.cpp
secp256k1.hpp
Expand Down
191 changes: 191 additions & 0 deletions lib/evmone_precompiles/modexp.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
#include "modexp.hpp"
#include <evmmax/evmmax.hpp>
#include <bit>
#include <span>

using namespace intx;

namespace
{
template <unsigned N>
void trunc(std::span<uint8_t> dst, const intx::uint<N>& x) noexcept
{
assert(dst.size() <= N / 8); // destination must be smaller than the source value
const auto d = to_big_endian(x);
std::copy_n(&as_bytes(d)[sizeof(d) - dst.size()], dst.size(), dst.begin());
}

template <typename UIntT>
UIntT modexp_odd(const UIntT& base, evmc::bytes_view exp, const UIntT& mod)
{
const evmmax::ModArith<UIntT> arith(mod);

UIntT ret = arith.to_mont(UIntT{1});
const auto base_mont = arith.to_mont(base);
// const auto base2 = arith.mul(base_mont, base_mont);
// const auto base3 = arith.mul(base_mont, base2);

for (const auto e : exp)
{
for (size_t i = 8; i != 0; --i)
{
ret = arith.mul(ret, ret);
const auto bit = e >> (i - 1) & 1;
if (bit != 0)
ret = arith.mul(ret, base_mont);
}
}

return arith.from_mont(ret);
}

template <typename UIntT>
UIntT modexp_pow_of_two(const UIntT& base, evmc::bytes_view exp, const UIntT& mod)
{
// FIXME: It should compute the value correctly for mod == 1, just checking if covered by tests.
assert(mod != 1);
UIntT ret = 1;
for (auto e : exp)
{
unsigned char mask = 0x80;
while (mask != 0)
{
ret *= ret;

Check warning on line 53 in lib/evmone_precompiles/modexp.cpp

View check run for this annotation

Codecov / codecov/patch

lib/evmone_precompiles/modexp.cpp#L53

Added line #L53 was not covered by tests
if ((mask & e) != 0)
ret *= base;

Check warning on line 55 in lib/evmone_precompiles/modexp.cpp

View check run for this annotation

Codecov / codecov/patch

lib/evmone_precompiles/modexp.cpp#L55

Added line #L55 was not covered by tests
mask >>= 1;
}
}

const auto mod_mask = mod - 1;
ret &= mod_mask;
return ret;
}

template <typename UIntT>
size_t ctz(const UIntT& value)

Check warning on line 66 in lib/evmone_precompiles/modexp.cpp

View check run for this annotation

Codecov / codecov/patch

lib/evmone_precompiles/modexp.cpp#L66

Added line #L66 was not covered by tests
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
size_t ctz(const UIntT& value)
unsigned ctz(const UIntT& value)

As in intx::clz().
Also move this function to the top of the file. We want to move it to intx later.

{
size_t mod_tailing_zeros = 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
size_t mod_tailing_zeros = 0;
unsigned num_tailing_zeros = 0;

for (size_t i = 0; i < value.num_words; ++i)

Check warning on line 69 in lib/evmone_precompiles/modexp.cpp

View check run for this annotation

Codecov / codecov/patch

lib/evmone_precompiles/modexp.cpp#L69

Added line #L69 was not covered by tests
{
if (value[i] == 0)

Check warning on line 71 in lib/evmone_precompiles/modexp.cpp

View check run for this annotation

Codecov / codecov/patch

lib/evmone_precompiles/modexp.cpp#L71

Added line #L71 was not covered by tests
{
mod_tailing_zeros += value.word_num_bits;

Check warning on line 73 in lib/evmone_precompiles/modexp.cpp

View check run for this annotation

Codecov / codecov/patch

lib/evmone_precompiles/modexp.cpp#L73

Added line #L73 was not covered by tests
continue;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The order in the loop is slightly confusing. Try doing: if (value[i] != 0) {...; break;} num_tailing_zeros += num_bits;.

}
else
{
mod_tailing_zeros += static_cast<size_t>(std::countr_zero(value[i]));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
mod_tailing_zeros += static_cast<size_t>(std::countr_zero(value[i]));
mod_tailing_zeros += intx::clz(value[i]);

break;

Check warning on line 79 in lib/evmone_precompiles/modexp.cpp

View check run for this annotation

Codecov / codecov/patch

lib/evmone_precompiles/modexp.cpp#L78-L79

Added lines #L78 - L79 were not covered by tests
}
}

return mod_tailing_zeros;

Check warning on line 83 in lib/evmone_precompiles/modexp.cpp

View check run for this annotation

Codecov / codecov/patch

lib/evmone_precompiles/modexp.cpp#L83

Added line #L83 was not covered by tests
}

template <typename UIntT>
UIntT modinv_2k(const UIntT& x, size_t k)

Check warning on line 87 in lib/evmone_precompiles/modexp.cpp

View check run for this annotation

Codecov / codecov/patch

lib/evmone_precompiles/modexp.cpp#L87

Added line #L87 was not covered by tests
{
UIntT b{1};
UIntT res;
for (size_t i = 0; i < k; ++i)

Check warning on line 91 in lib/evmone_precompiles/modexp.cpp

View check run for this annotation

Codecov / codecov/patch

lib/evmone_precompiles/modexp.cpp#L89-L91

Added lines #L89 - L91 were not covered by tests
{
UIntT t = b & UIntT{1};
b = (b - x * t) >> 1;
res += t << i;

Check warning on line 95 in lib/evmone_precompiles/modexp.cpp

View check run for this annotation

Codecov / codecov/patch

lib/evmone_precompiles/modexp.cpp#L93-L95

Added lines #L93 - L95 were not covered by tests
}

return res;

Check warning on line 98 in lib/evmone_precompiles/modexp.cpp

View check run for this annotation

Codecov / codecov/patch

lib/evmone_precompiles/modexp.cpp#L98

Added line #L98 was not covered by tests
}

template <typename UIntT>
UIntT modexp_impl(const UIntT& base, evmc::bytes_view exp, const UIntT& mod)
{
// FIXME: We should strip leading 0 bits/bytes of exp. The gas cost model requires it.

// is odd
if ((mod & UIntT{1}) == UIntT{1})
{
return modexp_odd(base, exp, mod);
}
else if ((mod << (clz(mod) + 1)) == 0) // is power of 2
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This way of checking "power of two" in unconventional. Create a helper function has_single_bit() to make the std:: variant.

For the implementation you can use one of the common bit tricks for now. See https://godbolt.org/z/qrKEEnnx5.

{
return modexp_pow_of_two(base, exp, mod);
}
else // is even
{
const auto mod_tailing_zeros = ctz(mod);

Check warning on line 117 in lib/evmone_precompiles/modexp.cpp

View check run for this annotation

Codecov / codecov/patch

lib/evmone_precompiles/modexp.cpp#L117

Added line #L117 was not covered by tests
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it make sense to create also modexp_even().


auto const N = mod >> mod_tailing_zeros;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This implementation requires a link to the algorithm. Also use lower case variable names and upper case constant names.

const UIntT K = UIntT{1} << mod_tailing_zeros;

Check warning on line 120 in lib/evmone_precompiles/modexp.cpp

View check run for this annotation

Codecov / codecov/patch

lib/evmone_precompiles/modexp.cpp#L119-L120

Added lines #L119 - L120 were not covered by tests

const auto x1 = modexp_odd(base, exp, N);
const auto x2 = modexp_pow_of_two(base, exp, K);

Check warning on line 123 in lib/evmone_precompiles/modexp.cpp

View check run for this annotation

Codecov / codecov/patch

lib/evmone_precompiles/modexp.cpp#L122-L123

Added lines #L122 - L123 were not covered by tests

const auto N_inv = modinv_2k(N, mod_tailing_zeros);

Check warning on line 125 in lib/evmone_precompiles/modexp.cpp

View check run for this annotation

Codecov / codecov/patch

lib/evmone_precompiles/modexp.cpp#L125

Added line #L125 was not covered by tests

return x1 + (((x2 - x1) * N_inv) % K) * N;

Check warning on line 127 in lib/evmone_precompiles/modexp.cpp

View check run for this annotation

Codecov / codecov/patch

lib/evmone_precompiles/modexp.cpp#L127

Added line #L127 was not covered by tests
}
}

template <typename UIntT>
UIntT load_from_bytes(evmc::bytes_view data)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The intx::load should handle this. If the API is not useful here, create a helper at the top of the file to move it to intx later.

{
constexpr auto num_bytes = UIntT::num_words * sizeof(typename UIntT::word_type);
assert(data.size() <= num_bytes);
if (data.size() == num_bytes)
{
return intx::be::unsafe::load<UIntT>(data.data());
}
else
{
evmc::bytes tmp;
tmp.resize(num_bytes);
std::memcpy(&tmp[num_bytes - data.size()], data.data(), data.size());
return intx::be::unsafe::load<UIntT>(tmp.data());
}
}

} // namespace

namespace evmone::crypto
{
bool modexp(evmc::bytes_view base, evmc::bytes_view exp, evmc::bytes_view mod, uint8_t* output)
{
static constexpr auto MAX_INPUT_SIZE = 1024;
if (base.size() > MAX_INPUT_SIZE || exp.size() > MAX_INPUT_SIZE || mod.size() > MAX_INPUT_SIZE)
return false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After EIP-7823 this is guaranteed so you can convert this to assert.


const auto size = std::max(mod.size(), base.size());

intx::uint<MAX_INPUT_SIZE * 8> max_res;
if (size <= 32)
{
max_res = modexp_impl(load_from_bytes<uint256>(base), exp, load_from_bytes<uint256>(mod));
}
else if (size <= 64)

Check warning on line 166 in lib/evmone_precompiles/modexp.cpp

View check run for this annotation

Codecov / codecov/patch

lib/evmone_precompiles/modexp.cpp#L166

Added line #L166 was not covered by tests
{
max_res = modexp_impl(load_from_bytes<uint512>(base), exp, load_from_bytes<uint512>(mod));

Check warning on line 168 in lib/evmone_precompiles/modexp.cpp

View check run for this annotation

Codecov / codecov/patch

lib/evmone_precompiles/modexp.cpp#L168

Added line #L168 was not covered by tests
}
else if (size <= 128)

Check warning on line 170 in lib/evmone_precompiles/modexp.cpp

View check run for this annotation

Codecov / codecov/patch

lib/evmone_precompiles/modexp.cpp#L170

Added line #L170 was not covered by tests
{
max_res = modexp_impl(
load_from_bytes<intx::uint<1024>>(base), exp, load_from_bytes<intx::uint<1024>>(mod));

Check warning on line 173 in lib/evmone_precompiles/modexp.cpp

View check run for this annotation

Codecov / codecov/patch

lib/evmone_precompiles/modexp.cpp#L172-L173

Added lines #L172 - L173 were not covered by tests
}
else if (size <= 256)

Check warning on line 175 in lib/evmone_precompiles/modexp.cpp

View check run for this annotation

Codecov / codecov/patch

lib/evmone_precompiles/modexp.cpp#L175

Added line #L175 was not covered by tests
{
max_res = modexp_impl(
load_from_bytes<intx::uint<2048>>(base), exp, load_from_bytes<intx::uint<2048>>(mod));

Check warning on line 178 in lib/evmone_precompiles/modexp.cpp

View check run for this annotation

Codecov / codecov/patch

lib/evmone_precompiles/modexp.cpp#L177-L178

Added lines #L177 - L178 were not covered by tests
}
else
{
max_res = modexp_impl(load_from_bytes<intx::uint<MAX_INPUT_SIZE * 8>>(base), exp,
load_from_bytes<intx::uint<MAX_INPUT_SIZE * 8>>(mod));

Check warning on line 183 in lib/evmone_precompiles/modexp.cpp

View check run for this annotation

Codecov / codecov/patch

lib/evmone_precompiles/modexp.cpp#L182-L183

Added lines #L182 - L183 were not covered by tests
}

trunc(std::span{output, mod.size()}, max_res);
return true;
}


} // namespace evmone::crypto
12 changes: 12 additions & 0 deletions lib/evmone_precompiles/modexp.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// evmone: Fast Ethereum Virtual Machine implementation
// Copyright 2025 The evmone Authors.
// SPDX-License-Identifier: Apache-2.0

#pragma once
#include <evmc/evmc.hpp>
#include <intx/intx.hpp>

namespace evmone::crypto
{
bool modexp(evmc::bytes_view base, evmc::bytes_view exp, evmc::bytes_view mod, uint8_t* output);
}
2 changes: 0 additions & 2 deletions test/state/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ target_sources(
precompiles.hpp
precompiles.cpp
precompiles_internal.hpp
precompiles_stubs.hpp
precompiles_stubs.cpp
requests.hpp
requests.cpp
rlp.hpp
Expand Down
28 changes: 22 additions & 6 deletions test/state/precompiles.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,22 @@
// SPDX-License-Identifier: Apache-2.0

#include "precompiles.hpp"

#include "precompiles_gmp.hpp"
#include "precompiles_internal.hpp"
#include "precompiles_stubs.hpp"
#include <evmone_precompiles/blake2b.hpp>
#include <evmone_precompiles/bls.hpp>
#include <evmone_precompiles/bn254.hpp>
#include <evmone_precompiles/kzg.hpp>
#include <evmone_precompiles/modexp.hpp>
#include <evmone_precompiles/ripemd160.hpp>
#include <evmone_precompiles/secp256k1.hpp>
#include <evmone_precompiles/sha256.hpp>
#include <intx/intx.hpp>
#include <array>
#include <bit>
#include <cassert>
#include <iostream>
#include <limits>
#include <span>

Expand Down Expand Up @@ -352,7 +355,7 @@
const auto mod_requires_padding = mod_explicit.size() != mod_len;
if (mod_requires_padding) [[unlikely]]
{
// The modulus is the last argument and some of its bytes may be missing and be implicitly
// The modulus is the last argument, and some of its bytes may be missing and be implicitly
// zero. In this case, copy the explicit modulus bytes to the output buffer and pad the rest
// with zeroes. The output buffer is guaranteed to have exactly the modulus size.
const auto [_, output_p] = std::ranges::copy(mod_explicit, output);
Expand All @@ -363,11 +366,24 @@
const auto exp = payload.substr(base_len, exp_len);
const auto mod = mod_requires_padding ? bytes_view{output, mod_len} : mod_explicit;

#ifdef EVMONE_PRECOMPILES_GMP
uint8_t evmone_output[1024]; // Use a separate buffer not to overwrite the truncated mod.
const auto in_range = crypto::modexp(base, exp, mod, evmone_output);
if (in_range)
{
uint8_t gmp_output[1024];
expmod_gmp(base, exp, mod, gmp_output);
if (std::memcmp(evmone_output, gmp_output, mod_len) != 0)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a temporary code for fuzzing. Maybe just move it under #ifndef NDEBUG (fuzzer compiles with asserts enabled).

{
std::cerr << "modexp output mismatch"
<< "\n evmone: " << evmc::hex({output, mod_len})
<< "\n gmp: " << evmc::hex({gmp_output, mod_len}) << '\n';
assert(false);

Check warning on line 380 in test/state/precompiles.cpp

View check run for this annotation

Codecov / codecov/patch

test/state/precompiles.cpp#L378-L380

Added lines #L378 - L380 were not covered by tests
}
std::copy_n(evmone_output, mod_len, output);
return {EVMC_SUCCESS, mod_len};
}

expmod_gmp(base, exp, mod, output);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new implementation is slower than GMP so it should replace the "stub", but GMP has to have the priority.

#else
expmod_stub(base, exp, mod, output);
#endif
return {EVMC_SUCCESS, mod.size()};
}

Expand Down
1 change: 1 addition & 0 deletions test/unittests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ target_sources(
evmmax_bn254_add_test.cpp
evmmax_bn254_mul_test.cpp
evmmax_bn254_pairing_test.cpp
evmmax_modexp.cpp
evmmax_test.cpp
evmmax_secp256k1_test.cpp
evmone_test.cpp
Expand Down
23 changes: 23 additions & 0 deletions test/unittests/evmmax_modexp.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// evmone: Fast Ethereum Virtual Machine implementation
// Copyright 2023 The evmone Authors.
// SPDX-License-Identifier: Apache-2.0

#include <evmone_precompiles/modexp.hpp>
#include <gtest/gtest.h>
#include <array>

#include <test/utils/utils.hpp>
using namespace evmone::test;

TEST(evmmax, modexp)

Check warning on line 12 in test/unittests/evmmax_modexp.cpp

View check run for this annotation

Codecov / codecov/patch

test/unittests/evmmax_modexp.cpp#L12

Added line #L12 was not covered by tests
{
{
evmc::bytes res;
res.resize(1025);
EXPECT_FALSE(evmone::crypto::modexp(

Check warning on line 17 in test/unittests/evmmax_modexp.cpp

View check run for this annotation

Codecov / codecov/patch

test/unittests/evmmax_modexp.cpp#L17

Added line #L17 was not covered by tests
"2000000000000000000000000000000000000000000000000000000000000000000000000000001020000000000000000000000000000000000000000000000000000000000000000000000000000010200000000000000000000000000000000000000000000000000000000000000000000000000000102000000000000000000000000000000000000000000000000000000000000000000000000000001020000000000000000000000000000000000000000000000000000000000000000000000000000010200000000000000000000000000000000000000000000000000000000000000000000000000000102000000000000000000000000000000000000000000000000000000000000000000000000000001020000000000000000000000000000000000000000000000000000000000000000000000000000010"_hex,
evmc::bytes{0x3},
"1060000000000000000000000000000010600000000000000000006000000000000000000000000000000000000000000000000000001060000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000106000000000000000000000000000000000000000000000000000000000000000000000000000001060000000000000000000000000000000000000000000000000000000000000000000000000000010600000000000000000000000000000000000000000000000000000000000000000000000000000106000000000000000000000000000000000000000000000000000000000000000000000000000001060000000000000000000000000000000000000000000000000000000000000000000000000000010600000000000000000000000000000000000000000000000000000000000000000000000000000106000000000000000000000000000000000000000000000000000000000000000000000000000001060000000000000000000000000000000000000000000000000000000000000000000000000000010600000000000000000000000000000000000000000000000000000000000000000000000000000106000000000000000000000000000000000000000000000000000000000000000000000000000001060000000000000000000000000000000000000000000000000000000000000000000000000000010600000000000000000000000000000000000000000000000000000000000000000000000000000106000000000000000000000000000000000000000000000000000000000000000000000000000001060000000000000000000000000000000000000000000000000000000000000000000000000000010600000000000000000000000000000000000000000000000000000000000000000000000000000106000000000000000000000000000000000000000000000000000000000000000000000000000001060000000000000000000000000000000000000000000000000000000000000000000000000000010600000000000000000000000000000000000000000000000000000000000000000000000000000106000000000000000000000000000000000000000000000000000000000000000000000000000001060000000000000000000000000000000000000000000000000000000000000000000000000000010600000000000000000000000000000000000000000000000000000000000000000000000000000106000000000000000000000000000000000000000000000000000000000000000000000000000001060000000000000000000000000000000000000000000000000000000000000000000000000000010"_hex,
res.data()));
}
}

Check warning on line 23 in test/unittests/evmmax_modexp.cpp

View check run for this annotation

Codecov / codecov/patch

test/unittests/evmmax_modexp.cpp#L23

Added line #L23 was not covered by tests
Loading