Skip to content

Commit 5845fbd

Browse files
committed
crypto: Implement wNAF MSM in ECC
- Replace Straus-Shamir MSM with windowed NAF (sliding window) method. - Set conservative initial windows size w=4. - Add NAF helper class, wNAF recoding, and shared MSM utility. - Cover NAF encoding/decoding with new crypto_wnaf unit tests. ### Benchmark results ``` │ o/ec.txt │ o/ec-wnaf-4.txt │ │ sec/op │ sec/op vs base │ precompile<PrecompileId::ecrecover,_evmmax_cpp>-14 126.3µ ± 0% 111.0µ ± 0% -12.13% (p=0.001 n=11) precompile<PrecompileId::ecmul,_evmmax_cpp>-14 53.91µ ± 0% 46.83µ ± 0% -13.13% (p=0.000 n=11) precompile<PrecompileId::p256verify,_evmone_cpp>-14 124.4µ ± 90% 107.2µ ± 90% -13.83% (p=0.028 n=11) geomean 94.61µ 82.28µ -13.03% │ o/ec.txt │ o/ec-wnaf-4.txt │ │ gas/op │ gas/op vs base │ precompile<PrecompileId::ecrecover,_evmmax_cpp>-14 30.00k ± 0% 30.00k ± 0% ~ (p=1.000 n=11) ¹ precompile<PrecompileId::ecmul,_evmmax_cpp>-14 60.00k ± 0% 60.00k ± 0% ~ (p=1.000 n=11) ¹ precompile<PrecompileId::p256verify,_evmone_cpp>-14 69.00k ± 0% 69.00k ± 0% ~ (p=1.000 n=11) ¹ geomean 49.89k 49.89k +0.00% ¹ all samples are equal │ o/ec.txt │ o/ec-wnaf-4.txt │ │ gas/s │ gas/s vs base │ precompile<PrecompileId::ecrecover,_evmmax_cpp>-14 23.75M ± 0% 27.04M ± 0% +13.86% (p=0.000 n=11) precompile<PrecompileId::ecmul,_evmmax_cpp>-14 111.3M ± 0% 128.1M ± 1% +15.09% (p=0.000 n=11) precompile<PrecompileId::p256verify,_evmone_cpp>-14 55.39M ± 0% 64.30M ± 0% +16.09% (p=0.000 n=11) geomean 52.71M 60.62M +15.01% │ o/ec.txt │ o/ec-wnaf-4.txt │ │ cycles/op │ cycles/op vs base │ precompile<PrecompileId::ecrecover,_evmmax_cpp>-14 503.5k ± 1% 441.4k ± 0% -12.33% (p=0.000 n=11) precompile<PrecompileId::ecmul,_evmmax_cpp>-14 214.9k ± 0% 186.5k ± 0% -13.22% (p=0.000 n=11) precompile<PrecompileId::p256verify,_evmone_cpp>-14 495.8k ± 90% 427.4k ± 0% -13.79% (p=0.010 n=11) geomean 377.1k 327.7k -13.11% │ o/ec.txt │ o/ec-wnaf-4.txt │ │ instructions/op │ instructions/op vs base │ precompile<PrecompileId::ecrecover,_evmmax_cpp>-14 1.537M ± 0% 1.382M ± 0% -10.12% (p=0.000 n=11) precompile<PrecompileId::ecmul,_evmmax_cpp>-14 737.5k ± 0% 663.6k ± 0% -10.03% (p=0.000 n=11) precompile<PrecompileId::p256verify,_evmone_cpp>-14 1.552M ± 0% 1.386M ± 0% -10.74% (p=0.000 n=11) geomean 1.207M 1.083M -10.29% ```
1 parent cf2bbe7 commit 5845fbd

3 files changed

Lines changed: 209 additions & 21 deletions

File tree

lib/evmone_precompiles/ecc.hpp

Lines changed: 101 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -492,40 +492,120 @@ ProjPoint<Curve> mul(const AffinePoint<Curve>& p, typename Curve::uint_type c) n
492492

493493
/// Computes multi-scalar multiplication of u×P ⊕ v×Q.
494494
///
495-
/// The implementation uses the "Straus-Shamir trick": https://eprint.iacr.org/2003/257.pdf#page=7.
496-
template <typename Curve>
497-
ProjPoint<Curve> msm(const typename Curve::uint_type& u, const AffinePoint<Curve>& p,
498-
const typename Curve::uint_type& v, const AffinePoint<Curve>& q)
495+
/// The implementation uses windowed NAF (wNAF) recoding.
496+
template <typename UIntT>
497+
class NAF
499498
{
500-
ProjPoint<Curve> r;
499+
public:
500+
using digit_type = int8_t;
501501

502-
const auto w = u | v;
503-
const auto bit_width = sizeof(w) * 8 - intx::clz(w);
504-
if (bit_width == 0)
505-
return r;
502+
private:
503+
/// The storage for the NAF digits, starting from the least significant one.
504+
/// For a k-bit scalar, there can be at most k+1 digits.
505+
std::array<digit_type, sizeof(UIntT) * 8 + 1> digits_{};
506506

507-
// Precompute affine P + Q. Works correctly if P == Q.
508-
const auto h = add_affine(p, q);
507+
/// The number of non-zero digits used in the representation.
508+
size_t width_ = 0;
509509

510-
// Create lookup table for points. The index 0 is unused.
511-
// TODO: Put 0 at index 0 and use it in the loop to avoid the branch.
512-
const AffinePoint<Curve>* const points[]{nullptr, &p, &q, &h};
510+
public:
511+
size_t width() const noexcept { return width_; }
513512

514-
for (auto i = bit_width; i != 0; --i)
513+
digit_type operator[](size_t i) const noexcept { return digits_[i]; }
514+
515+
void set(size_t i, digit_type d) noexcept
516+
{
517+
assert(d != 0);
518+
assert(i >= width_);
519+
digits_[i] = d;
520+
width_ = i + 1;
521+
}
522+
};
523+
524+
/// Convert an unsigned scalar value to its windowed Non-adjacent Form (wNAF).
525+
///
526+
/// See
527+
/// https://en.wikipedia.org/wiki/Elliptic_curve_point_multiplication#w-ary_non-adjacent_form_(wNAF)_method.
528+
template <unsigned W, typename UIntT>
529+
constexpr NAF<UIntT> to_wnaf(UIntT k) noexcept
530+
{
531+
using digit_type = NAF<UIntT>::digit_type;
532+
static_assert(W >= 2);
533+
static_assert(W <= sizeof(digit_type) * 8);
534+
constexpr unsigned RADIX = 1 << W;
535+
536+
NAF<UIntT> naf;
537+
for (size_t i = 0; k != 0; ++i, k >>= 1)
538+
{
539+
const auto r = static_cast<unsigned>(k) % RADIX;
540+
if (r % 2 != 0)
541+
{
542+
const auto d_sign = r > RADIX / 2;
543+
const auto d_abs = d_sign ? RADIX - r : r;
544+
const auto d = d_sign ? -d_abs : d_abs;
545+
naf.set(i, static_cast<digit_type>(d));
546+
k -= d_sign ? -UIntT{d_abs} : UIntT{d_abs}; // intx lacks sign extending conversion.
547+
}
548+
}
549+
return naf;
550+
}
551+
552+
template <unsigned W, typename Curve>
553+
void precompute_wnaf_table(
554+
std::span<ProjPoint<Curve>, 1 << (W - 2)> table, const AffinePoint<Curve>& p) noexcept
555+
{
556+
table[0] = ProjPoint{p}; // 1P.
557+
const auto two_p = dbl(table[0]); // 2P.
558+
559+
for (size_t i = 1; i < table.size(); ++i)
560+
table[i] = add(table[i - 1], two_p); // (2i+3)P = (2i+1)P + 2P.
561+
}
562+
563+
template <unsigned W, size_t S, typename Curve>
564+
ProjPoint<Curve> msm_wnaf(std::span<const AffinePoint<Curve>* const, S> points,
565+
std::span<const typename Curve::uint_type* const, S> scalars) noexcept
566+
{
567+
static constexpr size_t TABLE_SIZE = 1 << (W - 2);
568+
569+
std::array<NAF<typename Curve::uint_type>, S> nafs;
570+
std::array<ProjPoint<Curve>, S * TABLE_SIZE> joint_table;
571+
572+
for (size_t s = 0; s < S; ++s)
573+
{
574+
nafs[s] = to_wnaf<W>(*scalars[s]);
575+
precompute_wnaf_table<W>(
576+
std::span<ProjPoint<Curve>, TABLE_SIZE>{&joint_table[s * TABLE_SIZE], TABLE_SIZE},
577+
*points[s]);
578+
}
579+
580+
ProjPoint<Curve> r;
581+
const auto max_width =
582+
std::ranges::max(nafs, {}, &NAF<typename Curve::uint_type>::width).width();
583+
for (size_t i = max_width; i != 0; --i)
515584
{
516585
r = dbl(r);
517586

518-
const auto u_bit = bit_test(u, i - 1);
519-
const auto v_bit = bit_test(v, i - 1);
520-
const auto idx = 2 * size_t{v_bit} + size_t{u_bit};
521-
if (idx == 0)
522-
continue;
523-
r = add(r, *points[idx]);
587+
for (size_t s = 0; s < S; ++s)
588+
{
589+
const auto d = nafs[s][i - 1];
590+
if (d == 0) // TODO: likely
591+
continue;
592+
593+
const auto* table = &joint_table[s * TABLE_SIZE];
594+
const auto& pt = table[(static_cast<unsigned>(std::abs(d)) - 1) / 2];
595+
r = add(r, d >= 0 ? pt : -pt);
596+
}
524597
}
525598

526599
return r;
527600
}
528601

602+
template <typename Curve>
603+
ProjPoint<Curve> msm(const typename Curve::uint_type& u, const AffinePoint<Curve>& p,
604+
const typename Curve::uint_type& v, const AffinePoint<Curve>& q)
605+
{
606+
return msm_wnaf<4, 2, Curve>(std::array{&p, &q}, std::array{&u, &v});
607+
}
608+
529609
template <typename UIntT>
530610
struct SignedScalar
531611
{

test/unittests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ target_sources(
1010
baseline_analysis_test.cpp
1111
blockchaintest_loader_test.cpp
1212
bytecode_test.cpp
13+
crypto_wnaf.cpp
1314
evm_fixture.cpp
1415
evm_fixture.hpp
1516
evm_test.cpp

test/unittests/crypto_wnaf.cpp

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// evmone: Fast Ethereum Virtual Machine implementation
2+
// Copyright 2023 The evmone Authors.
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
#include <evmone_precompiles/ecc.hpp>
6+
#include <gtest/gtest.h>
7+
#include <intx/intx.hpp>
8+
#include <random>
9+
10+
using namespace evmmax::ecc;
11+
12+
namespace
13+
{
14+
template <typename UIntT>
15+
UIntT evaluate(NAF<UIntT> naf)
16+
{
17+
UIntT result = 0;
18+
UIntT base = 1;
19+
for (size_t i = 0; i < naf.width(); ++i)
20+
{
21+
const auto d = naf[i];
22+
const auto d_abs = static_cast<unsigned>(std::abs(d));
23+
const auto d_sign = d < 0;
24+
const auto r_abs = UIntT{d_abs};
25+
const auto r = d_sign ? -r_abs : r_abs;
26+
result += r * base;
27+
base <<= 1;
28+
}
29+
30+
if (naf.width() == 0)
31+
{
32+
// NAF == 0 <=> result == 0.
33+
EXPECT_EQ(result, 0);
34+
}
35+
else
36+
{
37+
// The most significant digit must be non-zero.
38+
EXPECT_NE(naf[naf.width() - 1], 0);
39+
}
40+
return result;
41+
}
42+
} // namespace
43+
44+
TEST(crypto_wnaf, example1)
45+
{
46+
const auto naf = to_wnaf<3>(uint32_t{21});
47+
EXPECT_EQ(naf.width(), 4u);
48+
EXPECT_EQ(naf[0], -3);
49+
EXPECT_EQ(naf[1], 0);
50+
EXPECT_EQ(naf[2], 0);
51+
EXPECT_EQ(naf[3], 3);
52+
EXPECT_EQ(naf[4], 0);
53+
EXPECT_EQ(evaluate(naf), 21u);
54+
}
55+
56+
TEST(crypto_wnaf, max_width)
57+
{
58+
const auto x = uint32_t{0xfffffffe};
59+
const auto naf = to_wnaf<4>(x);
60+
EXPECT_EQ(naf.width(), 33u);
61+
EXPECT_EQ(naf[0], 0);
62+
EXPECT_EQ(naf[1], -1);
63+
for (size_t i = 2; i <= 31; ++i)
64+
EXPECT_EQ(naf[i], 0);
65+
EXPECT_EQ(naf[32], 1);
66+
EXPECT_EQ(evaluate(naf), x);
67+
}
68+
69+
TEST(crypto_wnaf, max_digit)
70+
{
71+
const auto x = uint32_t{0xfffffcfe};
72+
const auto naf = to_wnaf<8>(x);
73+
EXPECT_EQ(naf.width(), 33u);
74+
EXPECT_EQ(naf[1], 127);
75+
EXPECT_EQ(evaluate(naf), x);
76+
}
77+
78+
TEST(crypto_wnaf, min_digit)
79+
{
80+
const auto x = uint32_t{0x102};
81+
const auto naf = to_wnaf<8>(x);
82+
EXPECT_EQ(naf.width(), 10u);
83+
EXPECT_EQ(naf[1], -127);
84+
EXPECT_EQ(evaluate(naf), x);
85+
}
86+
87+
TEST(crypto_wnaf, uint256_fuzz)
88+
{
89+
std::mt19937_64 rng{std::random_device{}()};
90+
std::uniform_int_distribution<uint64_t> dist{};
91+
const intx::uint256 start{dist(rng), dist(rng), dist(rng), dist(rng)};
92+
93+
for (size_t i = 0; i < 100; ++i)
94+
{
95+
const auto x = start + i;
96+
const auto naf2 = to_wnaf<2>(x);
97+
ASSERT_EQ(evaluate(naf2), x);
98+
const auto naf3 = to_wnaf<3>(x);
99+
ASSERT_EQ(evaluate(naf3), x);
100+
const auto naf4 = to_wnaf<4>(x);
101+
ASSERT_EQ(evaluate(naf4), x);
102+
const auto naf5 = to_wnaf<5>(x);
103+
ASSERT_EQ(evaluate(naf5), x);
104+
const auto naf8 = to_wnaf<8>(x);
105+
ASSERT_EQ(evaluate(naf8), x);
106+
}
107+
}

0 commit comments

Comments
 (0)