Skip to content

Commit 6533f2f

Browse files
rodiazetchfast
andcommitted
Optimize BN254 ecmul with field endomorphism
Add `ecc::decompose()` procedure to split ECC scalar into two smaller ones. Use the decomposition to speed up BN254 scalar multiplication. Co-authored-by: Paweł Bylica <pawel@hepcolgum.band>
1 parent eeb288a commit 6533f2f

4 files changed

Lines changed: 342 additions & 1 deletion

File tree

lib/evmone_precompiles/bn254.cpp

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,17 @@ bool validate(const AffinePoint& pt) noexcept
1818

1919
AffinePoint mul(const AffinePoint& pt, const uint256& c) noexcept
2020
{
21-
const auto pr = ecc::mul(pt, c);
21+
if (pt == 0)
22+
return pt;
23+
24+
if (c == 0)
25+
return {};
26+
27+
const auto [k1, k2] = ecc::decompose<Curve>(c);
28+
29+
const auto q = AffinePoint{Curve::BETA * pt.x, !k2.sign ? pt.y : -pt.y};
30+
const auto p = AffinePoint{pt.x, !k1.sign ? pt.y : -pt.y};
31+
const auto pr = msm(k1.value, p, k2.value, q);
2232
return ecc::to_affine(pr);
2333
}
2434
} // namespace evmmax::bn254

lib/evmone_precompiles/bn254.hpp

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,23 @@ struct Curve
3131

3232
static constexpr auto A = 0;
3333
static constexpr auto B = ecc::FieldElement<Curve>(3);
34+
35+
/// Endomorphism parameters. See ecc::decompose().
36+
/// @{
37+
/// λ
38+
static constexpr auto LAMBDA = 0xb3c4d79d41a917585bfc41088d8daaa78b17ea66b99c90dd_u256;
39+
/// β
40+
static constexpr ecc::FieldElement<Curve> BETA{
41+
0x59e26bcea0d48bacd4f263f1acdb5c4f5763473177fffffe_u256};
42+
/// 𝑥₁
43+
static constexpr auto X1 = 0x6f4d8248eeb859fd95b806bca6f338ee_u256;
44+
/// -𝑦₁
45+
static constexpr auto MINUS_Y1 = 0x6f4d8248eeb859fbf83e9682e87cfd45_u256;
46+
/// x₂
47+
static constexpr auto X2 = 0x6f4d8248eeb859fc8211bbeb7d4f1128_u256;
48+
/// 𝑦₂
49+
static constexpr auto Y2 = 0x6f4d8248eeb859fd0be4e1541221250b_u256;
50+
/// @}
3451
};
3552

3653
using AffinePoint = ecc::AffinePoint<Curve>;

lib/evmone_precompiles/ecc.hpp

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,4 +513,126 @@ ProjPoint<Curve> msm(const typename Curve::uint_type& u, const AffinePoint<Curve
513513
return r;
514514
}
515515

516+
template <typename UIntT>
517+
struct SignedScalar
518+
{
519+
bool sign = false; // The sign of the scalar: false = positive, true = negative.
520+
UIntT value;
521+
};
522+
523+
524+
/// Verifies k ≡ k₁ + k₂·λ (mod N) and checks that k₁ and k₂ are "short" scalars.
525+
template <typename Curve>
526+
[[maybe_unused, nodiscard]] bool verify_scalar_decomposition(const typename Curve::uint_type& k,
527+
const SignedScalar<typename Curve::uint_type>& k1,
528+
const SignedScalar<typename Curve::uint_type>& k2) noexcept
529+
{
530+
// Verify k ≡ k₁ + k₂·λ (mod N).
531+
{
532+
static constexpr ModArith N{Curve::ORDER};
533+
auto r_k1 = N.to_mont(k1.value);
534+
if (k1.sign)
535+
r_k1 = N.sub(0, r_k1);
536+
auto r_k2 = N.to_mont(k2.value);
537+
if (k2.sign)
538+
r_k2 = N.sub(0, r_k2);
539+
540+
const auto r_k = N.to_mont(k);
541+
542+
const auto right = N.add(r_k1, N.mul(r_k2, N.to_mont(Curve::LAMBDA)));
543+
if (r_k != right)
544+
return false;
545+
}
546+
547+
// Verify for u = (k₁, k₂) that ‖u‖ <= max(‖v₁‖, ‖v₂‖).
548+
{
549+
static constexpr auto V1_NORM_SQUARED =
550+
Curve::X1 * Curve::X1 + Curve::MINUS_Y1 * Curve::MINUS_Y1;
551+
static constexpr auto V2_NORM_SQUARED = Curve::X2 * Curve::X2 + Curve::Y2 * Curve::Y2;
552+
static constexpr auto MAX_NORM_SQUARED = std::max(V1_NORM_SQUARED, V2_NORM_SQUARED);
553+
const auto u_norm_squared = k1.value * k1.value + k2.value * k2.value;
554+
return u_norm_squared <= MAX_NORM_SQUARED;
555+
}
556+
}
557+
558+
/// Decomposes a scalar k into "short" scalars k₁ and k₂ such that k₁ + k₂·λ ≡ k (mod N).
559+
///
560+
/// This decomposition allows more efficient scalar multiplication by using the multi-scalar
561+
/// multiplication (MSM) and the GLV endomorphism.
562+
/// The endomorphism ϕ: E₂ → E₂ defined as (𝑥,𝑦) → (β𝑥,𝑦) with eigenvalue λ allows computing
563+
/// [λ](𝑥,𝑦) = (β𝑥,𝑦) with only one multiplication in 𝔽ₚ instead of a full scalar multiplication.
564+
///
565+
/// Moreover, to compute the short scalars k₁ and k₂, we need linearly independent short vectors
566+
/// (𝑣₁=(𝑥₁,𝑦₁), 𝑣₂=(x₂,𝑦₂)) such that f(𝑣₁) = f(𝑣₂) = 0,
567+
/// where f: ℤ×ℤ → ℤₙ is defined as (x,y) → (x + y·λ), where λ² + λ ≡ -1 mod N.
568+
///
569+
/// See https://www.iacr.org/archive/crypto2001/21390189.pdf for details.
570+
///
571+
/// The Curve type must provide the endomorphism parameters: LAMBDA, BETA, X1, MINUS_Y1, X2, Y2.
572+
template <typename Curve>
573+
std::array<SignedScalar<typename Curve::uint_type>, 2> decompose(
574+
const typename Curve::uint_type& k) noexcept
575+
{
576+
using UIntT = Curve::uint_type;
577+
578+
// Validate the provided setup parameters.
579+
// λ² + λ ≡ -1 mod n
580+
static_assert((umul(Curve::LAMBDA, Curve::LAMBDA) + Curve::LAMBDA + 1) % Curve::ORDER == 0);
581+
// f: (x, y) → (x + λy) mod N
582+
// f(v₁) = 0
583+
static_assert(
584+
(Curve::X1 + umul(Curve::ORDER - Curve::MINUS_Y1, Curve::LAMBDA)) % Curve::ORDER == 0);
585+
// f(v₂) = 0
586+
static_assert((Curve::X2 + umul(Curve::Y2, Curve::LAMBDA)) % Curve::ORDER == 0);
587+
588+
static constexpr auto round_div = [](const auto& a) noexcept {
589+
// DET is the (𝑣₁, 𝑣₂) matrix determinant.
590+
static constexpr auto WIDE_DET =
591+
umul(Curve::X1, Curve::Y2) + umul(Curve::X2, Curve::MINUS_Y1);
592+
static_assert(WIDE_DET <= std::numeric_limits<UIntT>::max());
593+
static constexpr auto DET = static_cast<UIntT>(WIDE_DET);
594+
static constexpr auto HALF_DET = DET / 2;
595+
596+
const auto [wide_q, r] = udivrem(a, DET);
597+
// Division reduces the quotient enough to fit into a single uint.
598+
// This can be shown at compile-time by inspecting the DET and Y2/-Y1 values.
599+
assert(wide_q < std::numeric_limits<UIntT>::max());
600+
const auto q = static_cast<UIntT>(wide_q);
601+
return q + (r > HALF_DET); // Round to nearest.
602+
};
603+
604+
// Solve a system of two equations using Cramer method.
605+
// ⎡X1 X2⎤ * ⎡b1⎤ = ⎡k⎤
606+
// ⎣Y1 Y2⎦ ⎣b2⎦ ⎣0⎦
607+
// and then approximate to the nearest integers:
608+
// b1 = ⌊ Y2·k ÷ DET⌉
609+
// b2 = ⌊-Y1·k ÷ DET⌉
610+
const auto b1 = round_div(umul(k, Curve::Y2));
611+
const auto b2 = round_div(umul(k, Curve::MINUS_Y1));
612+
613+
// k1 = k - (x1*b1 + x2*b2)
614+
const auto x1b1_x2b2 = umul(b1, Curve::X1) + umul(b2, Curve::X2);
615+
const auto [wide_k1, k1_is_neg] = subc(decltype(x1b1_x2b2){k}, x1b1_x2b2);
616+
const auto k1_abs = k1_is_neg ? -static_cast<UIntT>(wide_k1) : static_cast<UIntT>(wide_k1);
617+
618+
// k2 = 0 - (y1*b1 + y2*b2)
619+
const auto [wide_k2, k2_is_neg] = subc(umul(b1, Curve::MINUS_Y1), umul(b2, Curve::Y2));
620+
const auto k2_abs = k2_is_neg ? -static_cast<UIntT>(wide_k2) : static_cast<UIntT>(wide_k2);
621+
622+
const SignedScalar k1{k1_is_neg, k1_abs};
623+
const SignedScalar k2{k2_is_neg, k2_abs};
624+
assert(verify_scalar_decomposition<Curve>(k, k1, k2));
625+
626+
// FIXME: Bounds for fuzzing, remove.
627+
static_assert(Curve::Y2 < Curve::X1);
628+
static_assert(Curve::X2 < Curve::Y2);
629+
static_assert(Curve::MINUS_Y1 < Curve::X2);
630+
[[maybe_unused]] static constexpr auto K1_MAX = Curve::Y2;
631+
[[maybe_unused]] static constexpr auto K2_MAX = Curve::X2 - 1;
632+
assert(k1.value <= K1_MAX);
633+
assert(k2.value <= K2_MAX);
634+
635+
return {k1, k2};
636+
}
637+
516638
} // namespace evmmax::ecc

test/unittests/evmmax_bn254_mul_test.cpp

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,198 @@
99
using namespace evmmax::bn254;
1010
using namespace evmone::test;
1111

12+
TEST(evmmax, bn254_decompose)
13+
{
14+
struct TestCase
15+
{
16+
uint256 k;
17+
bool sign1 = false;
18+
uint256 k1;
19+
bool sign2 = false;
20+
uint256 k2;
21+
};
22+
23+
static const std::vector<TestCase> TEST_CASES = {
24+
{0, false, 0, false, 0},
25+
{1, false, 1, false, 0},
26+
{
27+
// FIXME: Shouldn't these be (0, 1)?
28+
Curve::LAMBDA,
29+
false,
30+
0x89d3256894d213e3,
31+
true,
32+
Curve::X2 - 1,
33+
},
34+
{
35+
Curve::ORDER - Curve::LAMBDA,
36+
false,
37+
0,
38+
true,
39+
1,
40+
},
41+
{
42+
2 * Curve::ORDER, // DET
43+
false,
44+
0,
45+
false,
46+
0,
47+
},
48+
{
49+
2 * Curve::ORDER - 1, // DET-1
50+
true,
51+
1,
52+
false,
53+
0,
54+
},
55+
{
56+
2 * Curve::ORDER + 1, // DET+1
57+
false,
58+
1,
59+
false,
60+
0,
61+
},
62+
{
63+
Curve::ORDER, // DET/2
64+
false,
65+
Curve::Y2,
66+
false,
67+
0x89d3256894d213e3,
68+
},
69+
{
70+
Curve::ORDER - 1, // DET/2-1
71+
false,
72+
Curve::Y2 - 1,
73+
false,
74+
0x89d3256894d213e3,
75+
},
76+
{
77+
Curve::ORDER + 1, // DET/2+1
78+
true,
79+
Curve::Y2 - 1,
80+
true,
81+
0x89d3256894d213e3,
82+
},
83+
{
84+
0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff_u256,
85+
true,
86+
0x272d9e49b8c8ca4335756fc61411a7a3_u128,
87+
false,
88+
0x3f296ebc4b455178a6a2b71572d476d6_u128,
89+
},
90+
{
91+
0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff_u256 % Curve::ORDER,
92+
true,
93+
0x272d9e49b8c8ca42aba24a5d7f3f93c0_u128,
94+
true,
95+
0x3024138ca3730883db6f04d60a7a9a52_u128,
96+
},
97+
{
98+
0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff_u256 %
99+
Curve::FIELD_PRIME,
100+
true,
101+
0x272d9e49b8c8ca3dd335f9b043dce0ca_u128,
102+
false,
103+
0x3f296ebc4b45517b57c272205aeeda45_u128,
104+
},
105+
{
106+
Curve::X1,
107+
false,
108+
0,
109+
false,
110+
Curve::MINUS_Y1,
111+
},
112+
{
113+
Curve::X2,
114+
false,
115+
Curve::X2,
116+
false,
117+
0,
118+
},
119+
{
120+
Curve::MINUS_Y1,
121+
false,
122+
Curve::MINUS_Y1,
123+
false,
124+
0,
125+
},
126+
{
127+
Curve::Y2,
128+
true,
129+
0x89d3256894d213e3,
130+
false,
131+
Curve::MINUS_Y1,
132+
},
133+
// Fuzzer finds:
134+
{
135+
0x30644e72e131a029b85045b68181585d2833e84879b9709143e1fd91c6ea5404_u256,
136+
true,
137+
0x6f4d8248eeb859fd0be4d9563b36d108_u128,
138+
true,
139+
0x89d3256894d213e3,
140+
},
141+
{
142+
0x30644e72e131a029b85045b68181585d9781875b000000000000000000000000_u256,
143+
false,
144+
0x1cc9978e3571b0392917fddedaf4_u128,
145+
true,
146+
0x89d3256894d213e3,
147+
},
148+
{
149+
0x00b3c4d79d41a917585bfc41088d8daaa78b17e6af48a03bbfd25e8cd0364141_u256,
150+
true,
151+
0x3b770fc551d2da1732fc9bebf_u128,
152+
false,
153+
0x100000000000000,
154+
},
155+
{
156+
0x30644e72e131a02a6c151d53c32a6fb58431295107473e18805b7c56ba7d94de_u256,
157+
false,
158+
0x10000000022dfb1619c5c10e10400_u128,
159+
false,
160+
1,
161+
},
162+
{
163+
Curve::ORDER - Curve::LAMBDA,
164+
false,
165+
0,
166+
true,
167+
1,
168+
},
169+
{
170+
0xfffffffffc2f0000000000000000000000000000000000000000000000000000_u256,
171+
true,
172+
0x4b3beb451625cff3c66c0effee355638_u128,
173+
true,
174+
0x11e8b394e6bd3d13de2f7f7d38887a8c_u128,
175+
},
176+
{
177+
0x000000000000000059e26bcea0d48bac65a4e1a8be2302524b7e65dd65dedb2a_u256,
178+
false,
179+
0x36,
180+
true,
181+
0x44e992b44a6909f1,
182+
},
183+
};
184+
185+
static constexpr auto decompose = evmmax::ecc::decompose<Curve>;
186+
187+
for (const auto& t : TEST_CASES)
188+
{
189+
const auto& [first, second] = decompose(t.k);
190+
const auto& [sign1, k1] = first;
191+
const auto& [sign2, k2] = second;
192+
193+
SCOPED_TRACE(hex(t.k));
194+
195+
EXPECT_EQ(sign1, t.sign1);
196+
EXPECT_EQ(k1, t.k1) << hex(k1) << " != " << hex(t.k1);
197+
EXPECT_EQ(sign2, t.sign2);
198+
EXPECT_EQ(k2, t.k2) << hex(k2) << " != " << hex(t.k2);
199+
200+
EXPECT_TRUE(verify_scalar_decomposition<Curve>(t.k, first, second));
201+
}
202+
}
203+
12204
namespace
13205
{
14206
struct TestCase

0 commit comments

Comments
 (0)