-
Notifications
You must be signed in to change notification settings - Fork 327
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
base: master
Are you sure you want to change the base?
evmmax: Implement modexp #1135
Changes from all commits
743287f
422b324
adf6e28
8aeda91
7a01869
5dd8b95
6cc1e41
7564901
2f093ea
b747b2b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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; | ||||||
if ((mask & e) != 0) | ||||||
ret *= base; | ||||||
mask >>= 1; | ||||||
} | ||||||
} | ||||||
|
||||||
const auto mod_mask = mod - 1; | ||||||
ret &= mod_mask; | ||||||
return ret; | ||||||
} | ||||||
|
||||||
template <typename UIntT> | ||||||
size_t ctz(const UIntT& value) | ||||||
{ | ||||||
size_t mod_tailing_zeros = 0; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
for (size_t i = 0; i < value.num_words; ++i) | ||||||
{ | ||||||
if (value[i] == 0) | ||||||
{ | ||||||
mod_tailing_zeros += value.word_num_bits; | ||||||
continue; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The order in the loop is slightly confusing. Try doing: |
||||||
} | ||||||
else | ||||||
{ | ||||||
mod_tailing_zeros += static_cast<size_t>(std::countr_zero(value[i])); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
break; | ||||||
} | ||||||
} | ||||||
|
||||||
return mod_tailing_zeros; | ||||||
} | ||||||
|
||||||
template <typename UIntT> | ||||||
UIntT modinv_2k(const UIntT& x, size_t k) | ||||||
{ | ||||||
UIntT b{1}; | ||||||
UIntT res; | ||||||
for (size_t i = 0; i < k; ++i) | ||||||
{ | ||||||
UIntT t = b & UIntT{1}; | ||||||
b = (b - x * t) >> 1; | ||||||
res += t << i; | ||||||
} | ||||||
|
||||||
return res; | ||||||
} | ||||||
|
||||||
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 | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it make sense to create also |
||||||
|
||||||
auto const N = mod >> mod_tailing_zeros; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||||||
|
||||||
const auto x1 = modexp_odd(base, exp, N); | ||||||
const auto x2 = modexp_pow_of_two(base, exp, K); | ||||||
|
||||||
const auto N_inv = modinv_2k(N, mod_tailing_zeros); | ||||||
|
||||||
return x1 + (((x2 - x1) * N_inv) % K) * N; | ||||||
} | ||||||
} | ||||||
|
||||||
template <typename UIntT> | ||||||
UIntT load_from_bytes(evmc::bytes_view data) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||||||
{ | ||||||
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; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||||||
{ | ||||||
max_res = modexp_impl(load_from_bytes<uint512>(base), exp, load_from_bytes<uint512>(mod)); | ||||||
} | ||||||
else if (size <= 128) | ||||||
{ | ||||||
max_res = modexp_impl( | ||||||
load_from_bytes<intx::uint<1024>>(base), exp, load_from_bytes<intx::uint<1024>>(mod)); | ||||||
} | ||||||
else if (size <= 256) | ||||||
{ | ||||||
max_res = modexp_impl( | ||||||
load_from_bytes<intx::uint<2048>>(base), exp, load_from_bytes<intx::uint<2048>>(mod)); | ||||||
} | ||||||
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)); | ||||||
} | ||||||
|
||||||
trunc(std::span{output, mod.size()}, max_res); | ||||||
return true; | ||||||
} | ||||||
|
||||||
|
||||||
} // namespace evmone::crypto |
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); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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> | ||
|
||
|
@@ -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); | ||
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a temporary code for fuzzing. Maybe just move it under |
||
{ | ||
std::cerr << "modexp output mismatch" | ||
<< "\n evmone: " << evmc::hex({output, mod_len}) | ||
<< "\n gmp: " << evmc::hex({gmp_output, mod_len}) << '\n'; | ||
assert(false); | ||
} | ||
std::copy_n(evmone_output, mod_len, output); | ||
return {EVMC_SUCCESS, mod_len}; | ||
} | ||
|
||
expmod_gmp(base, exp, mod, output); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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()}; | ||
} | ||
|
||
|
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) | ||
{ | ||
{ | ||
evmc::bytes res; | ||
res.resize(1025); | ||
EXPECT_FALSE(evmone::crypto::modexp( | ||
"2000000000000000000000000000000000000000000000000000000000000000000000000000001020000000000000000000000000000000000000000000000000000000000000000000000000000010200000000000000000000000000000000000000000000000000000000000000000000000000000102000000000000000000000000000000000000000000000000000000000000000000000000000001020000000000000000000000000000000000000000000000000000000000000000000000000000010200000000000000000000000000000000000000000000000000000000000000000000000000000102000000000000000000000000000000000000000000000000000000000000000000000000000001020000000000000000000000000000000000000000000000000000000000000000000000000000010"_hex, | ||
evmc::bytes{0x3}, | ||
"1060000000000000000000000000000010600000000000000000006000000000000000000000000000000000000000000000000000001060000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000106000000000000000000000000000000000000000000000000000000000000000000000000000001060000000000000000000000000000000000000000000000000000000000000000000000000000010600000000000000000000000000000000000000000000000000000000000000000000000000000106000000000000000000000000000000000000000000000000000000000000000000000000000001060000000000000000000000000000000000000000000000000000000000000000000000000000010600000000000000000000000000000000000000000000000000000000000000000000000000000106000000000000000000000000000000000000000000000000000000000000000000000000000001060000000000000000000000000000000000000000000000000000000000000000000000000000010600000000000000000000000000000000000000000000000000000000000000000000000000000106000000000000000000000000000000000000000000000000000000000000000000000000000001060000000000000000000000000000000000000000000000000000000000000000000000000000010600000000000000000000000000000000000000000000000000000000000000000000000000000106000000000000000000000000000000000000000000000000000000000000000000000000000001060000000000000000000000000000000000000000000000000000000000000000000000000000010600000000000000000000000000000000000000000000000000000000000000000000000000000106000000000000000000000000000000000000000000000000000000000000000000000000000001060000000000000000000000000000000000000000000000000000000000000000000000000000010600000000000000000000000000000000000000000000000000000000000000000000000000000106000000000000000000000000000000000000000000000000000000000000000000000000000001060000000000000000000000000000000000000000000000000000000000000000000000000000010600000000000000000000000000000000000000000000000000000000000000000000000000000106000000000000000000000000000000000000000000000000000000000000000000000000000001060000000000000000000000000000000000000000000000000000000000000000000000000000010"_hex, | ||
res.data())); | ||
} | ||
} | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As in
intx::clz()
.Also move this function to the top of the file. We want to move it to intx later.