Barretenberg
The ZK-SNARK library at the core of Aztec
Loading...
Searching...
No Matches
wnaf.test.cpp
Go to the documentation of this file.
1#include "wnaf.hpp"
2#include "../curves/bn254/fr.hpp"
4#include "booth_recode.hpp"
5#include <array>
6#include <gtest/gtest.h>
7#include <utility>
8
9using namespace bb;
10
11namespace {
13}
14
15// NOLINTBEGIN(cppcoreguidelines-avoid-c-arrays)
16namespace {
17
18void recover_fixed_wnaf(const uint64_t* wnaf, bool skew, uint64_t& hi, uint64_t& lo, size_t wnaf_bits)
19{
20 const size_t wnaf_entries = (127 + wnaf_bits - 1) / wnaf_bits;
21 const uint64_t max_table_index = (1UL << (wnaf_bits - 1)) - 1;
22
23 for (size_t i = 0; i < wnaf_entries; ++i) {
24 uint64_t entry = wnaf[i];
25 uint64_t table_index = entry & 0x7fffffffUL;
26 bool sign = ((entry >> 31) & 1) != 0U;
27 uint64_t point_index_bits = entry >> 32;
28
29 EXPECT_LE(table_index, max_table_index)
30 << "entry " << i << ": table_index " << table_index << " exceeds max " << max_table_index;
31
32 // The most significant digit is always positive by construction (no sign bit is OR'd in).
33 if (i == 0) {
34 EXPECT_FALSE(sign) << "entry 0 (most significant digit) must be positive";
35 }
36
37 // All current callers use point_index=0, so bits 32-63 should be clear.
38 EXPECT_EQ(point_index_bits, 0UL) << "entry " << i << ": unexpected non-zero point_index bits";
39 }
40
41 // Recover the scalar: sum signed odd digits at their positional weights, then subtract skew.
42 uint128_t scalar = 0;
43 for (size_t i = 0; i < wnaf_entries; ++i) {
44 uint64_t entry_formatted = wnaf[i];
45 bool negative = ((entry_formatted >> 31) & 1) != 0U;
46 uint64_t digit = ((entry_formatted & 0x7fffffffUL) << 1) + 1;
47 auto shift = static_cast<uint128_t>(wnaf_bits * (wnaf_entries - 1 - i));
48 if (negative) {
49 scalar -= static_cast<uint128_t>(digit) << shift;
50 } else {
51 scalar += static_cast<uint128_t>(digit) << shift;
52 }
53 }
54 scalar -= static_cast<uint128_t>(skew);
55 hi = static_cast<uint64_t>(scalar >> static_cast<uint128_t>(64));
56 lo = static_cast<uint64_t>(scalar & static_cast<uint128_t>(0xffff'ffff'ffff'ffff));
57}
58
59// Slow bit-by-bit oracle for the Booth digit reader; intentionally avoids the production limb/mask decomposition.
60uint32_t compute_booth_packed_digit_bit_by_bit(const uint64_t* scalar, size_t bit_offset, size_t window_bits)
61{
62 uint32_t raw = 0;
63 for (size_t i = 0; i <= window_bits; ++i) {
64 if (bit_offset == 0 && i == 0) {
65 continue;
66 }
67 const size_t bit = bit_offset + i - 1;
68 raw |= static_cast<uint32_t>((scalar[bit / 64] >> (bit & 63)) & 1ULL) << i;
69 }
70 const uint32_t neg = (raw >> window_bits) & uint32_t{ 1 };
71 const uint32_t neg_mask = uint32_t{ 0 } - neg;
72 const uint32_t val_mask = (uint32_t{ 1 } << window_bits) - 1;
73 const uint32_t encode = (raw + 1) >> 1;
74 const uint32_t magnitude = ((encode + neg_mask) ^ neg_mask) & val_mask;
75 return (neg << 31) | magnitude;
76}
77} // namespace
78
79TEST(wnaf, GetWnafBitsConstLimbBoundary)
80{
81 // scalar[0] bits 59-63 = 1,0,1,0,1 and scalar[1] bits 0-4 = 1,0,1,0,1
82 // Full bit pattern around the boundary (bit 63 | bit 64):
83 // bit: ...59 60 61 62 63 | 64 65 66 67 68 69...
84 // val: ... 1 0 1 0 1 | 1 0 1 0 1 0...
85 const uint64_t scalar[2] = { 0xA800000000000000ULL, 0x0000000000000015ULL };
86
87 // Window starts at bit 63 — straddles the limb boundary (2 bits from lo, 3 from hi)
88 // bits 63,64,65,66,67 = 1,1,0,1,0 → 1 + 2 + 0 + 8 + 0 = 11
89 EXPECT_EQ((wnaf::get_wnaf_bits_const<5, 63>(scalar)), 11ULL);
90
91 // Window starts at bit 64 — exactly at the hi limb start
92 // bits 64,65,66,67,68 = 1,0,1,0,1 → 1 + 0 + 4 + 0 + 16 = 21
93 EXPECT_EQ((wnaf::get_wnaf_bits_const<5, 64>(scalar)), 21ULL);
94
95 // Window starts at bit 65 — one past the boundary, entirely in hi limb
96 // bits 65,66,67,68,69 = 0,1,0,1,0 → 0 + 2 + 0 + 8 + 0 = 10
97 EXPECT_EQ((wnaf::get_wnaf_bits_const<5, 65>(scalar)), 10ULL);
98}
99
100TEST(booth_recode, PackedDigitMatchesReference)
101{
102 const uint64_t scalar[2] = { 0xf0e1d2c3b4a59687ULL, 0x8070605040302010ULL };
103
104 for (const size_t window_bits : { size_t{ 2 }, size_t{ 4 } }) {
105 for (const size_t bit_offset : { size_t{ 0 },
106 size_t{ 2 },
107 size_t{ 4 },
108 size_t{ 60 },
109 size_t{ 62 },
110 size_t{ 63 },
111 size_t{ 64 },
112 size_t{ 65 },
113 size_t{ 124 } }) {
114 const auto params = ecc::booth::compute_booth_slice_params(bit_offset, window_bits, 2);
115 EXPECT_EQ(ecc::booth::booth_packed_digit(scalar, params, window_bits),
116 compute_booth_packed_digit_bit_by_bit(scalar, bit_offset, window_bits))
117 << "window_bits=" << window_bits << ", bit_offset=" << bit_offset;
118 }
119 }
120}
121
122TEST(booth_recode, TopWindowClampsHighLimb)
123{
124 const uint64_t scalar[2] = { 0, 1ULL << 63 };
125 const auto params = ecc::booth::compute_booth_slice_params(124, 4, 2);
126
127 EXPECT_EQ(params.lo_limb, 1U);
128 EXPECT_EQ(params.hi_limb, 1U);
129 EXPECT_EQ(params.hi_mask, 0U);
130 EXPECT_TRUE(params.slice_localised_to_one_u64);
131 EXPECT_EQ(ecc::booth::booth_packed_digit(scalar, params, 4), compute_booth_packed_digit_bit_by_bit(scalar, 124, 4));
132}
133
134TEST(EndomorphismSplit, SmallScalarFastPathBoundaries)
135{
137 std::pair{ fr{ 0, 0, 0, 0 }, std::array<uint64_t, 2>{ 0, 0 } },
138 std::pair{ fr{ 1, 0, 0, 0 }, std::array<uint64_t, 2>{ 1, 0 } },
139 std::pair{ fr{ 0, 1ULL << 62, 0, 0 }, std::array<uint64_t, 2>{ 0, 1ULL << 62 } },
140 std::pair{ fr{ ~uint64_t{ 0 }, (1ULL << 63) - 1, 0, 0 },
141 std::array<uint64_t, 2>{ ~uint64_t{ 0 }, (1ULL << 63) - 1 } },
142 };
143
144 for (const auto& [scalar, expected_k1] : test_cases) {
145 const auto [k1, k2] = fr::split_into_endomorphism_scalars(scalar);
146 EXPECT_EQ(k1, expected_k1);
147 EXPECT_EQ(k2, (std::array<uint64_t, 2>{ 0, 0 }));
148 }
149}
150
151TEST(EndomorphismSplit, BoundaryAtTwoTo127UsesGeneralPath)
152{
153 const fr scalar{ 0, 1ULL << 63, 0, 0 };
154 const auto [k1, k2] = fr::split_into_endomorphism_scalars(scalar);
155
156 EXPECT_NE(k2, (std::array<uint64_t, 2>{ 0, 0 }));
157
158 fr k1_field{ k1[0], k1[1], 0, 0 };
159 fr k2_field{ k2[0], k2[1], 0, 0 };
160 const fr recovered = k1_field - (k2_field * fr::cube_root_of_unity());
161 EXPECT_EQ(recovered, scalar);
162}
163
164TEST(wnaf, WnafPowerOfTwo)
165{
166 // Powers of 2 are all even (skew = true) and have a single 1-bit with all lower bits zero,
167 // so every window below the leading bit is even, forcing borrows to cascade through all rounds.
168 auto test_power_of_two_scalar = [](uint64_t lo, uint64_t hi) {
169 uint64_t buffer[2] = { lo, hi };
170 uint64_t wnaf_out[WNAF_SIZE(5)] = { 0 };
171 bool skew = false;
172 wnaf::fixed_wnaf<1, 5>(buffer, wnaf_out, skew, 0);
173 EXPECT_TRUE(skew); // all powers of 2 are even
174 uint64_t recovered_hi = 0;
175 uint64_t recovered_lo = 0;
176 recover_fixed_wnaf(wnaf_out, skew, recovered_hi, recovered_lo, 5);
177 EXPECT_EQ(lo, recovered_lo);
178 EXPECT_EQ(hi, recovered_hi);
179 };
180
181 test_power_of_two_scalar(2ULL, 0ULL); // 2^1: smallest even, borrows cascade through all 26 windows
182 test_power_of_two_scalar(1ULL << 32, 0ULL); // 2^32: mid-lo-limb
183 test_power_of_two_scalar(0ULL, 1ULL); // 2^64: exactly at the limb boundary
184 test_power_of_two_scalar(0ULL, 1ULL << 62); // 2^126: near the 127-bit maximum
185}
186
187TEST(wnaf, WnafZero)
188{
189 uint64_t buffer[2]{ 0, 0 };
190 uint64_t wnaf[WNAF_SIZE(5)] = { 0 };
191 bool skew = false;
192 wnaf::fixed_wnaf<1, 5>(buffer, wnaf, skew, 0);
193 uint64_t recovered_hi = 0;
194 uint64_t recovered_lo = 0;
195 recover_fixed_wnaf(wnaf, skew, recovered_hi, recovered_lo, 5);
196 EXPECT_EQ(recovered_lo, 0UL);
197 EXPECT_EQ(recovered_hi, 0UL);
198 EXPECT_EQ(buffer[0], recovered_lo);
199 EXPECT_EQ(buffer[1], recovered_hi);
200}
201
202TEST(wnaf, WnafTwoBitWindow)
203{
208 constexpr uint32_t window = 2;
209 constexpr uint32_t num_bits = 254;
210 constexpr uint32_t num_quads = (num_bits >> 1) + 1;
211 uint64_t wnaf[num_quads] = { 0 };
212 bool skew = false;
213 wnaf::fixed_wnaf<256, 1, window>(&input.data[0], wnaf, skew, 0);
214
239 uint256_t recovered = 0;
240 uint256_t four_power = (uint256_t(1) << num_bits);
241 for (uint64_t i : wnaf) {
242 int extracted = 2 * (static_cast<int>(i) & 1) + 1;
243 bool sign = (i >> 31) == 0;
244
245 if (sign) {
246 recovered += uint256_t(static_cast<uint64_t>(extracted)) * four_power;
247 } else {
248 recovered -= uint256_t(static_cast<uint64_t>(extracted)) * four_power;
249 }
250 four_power >>= 2;
251 }
252 recovered -= static_cast<uint64_t>(skew);
253 EXPECT_EQ(recovered, input);
254}
255
256TEST(wnaf, WnafFixed)
257{
259 buffer.data[1] &= 0x7fffffffffffffffUL;
260 uint64_t wnaf[WNAF_SIZE(5)] = { 0 };
261 bool skew = false;
262 wnaf::fixed_wnaf<1, 5>(&buffer.data[0], wnaf, skew, 0);
263 uint64_t recovered_hi = 0;
264 uint64_t recovered_lo = 0;
265 recover_fixed_wnaf(wnaf, skew, recovered_hi, recovered_lo, 5);
266 EXPECT_EQ(buffer.data[0], recovered_lo);
267 EXPECT_EQ(buffer.data[1], recovered_hi);
268}
269
270TEST(wnaf, WnafFixedSimpleLo)
271{
272 uint64_t rand_buffer[2]{ 1, 0 };
273 uint64_t wnaf[WNAF_SIZE(5)]{ 0 };
274 bool skew = false;
275 wnaf::fixed_wnaf<1, 5>(rand_buffer, wnaf, skew, 0);
276 uint64_t recovered_hi = 0;
277 uint64_t recovered_lo = 0;
278 recover_fixed_wnaf(wnaf, skew, recovered_hi, recovered_lo, 5);
279 EXPECT_EQ(rand_buffer[0], recovered_lo);
280 EXPECT_EQ(rand_buffer[1], recovered_hi);
281}
282
283TEST(wnaf, WnafFixedSimpleHi)
284{
285 uint64_t rand_buffer[2] = { 0, 1 };
286 uint64_t wnaf[WNAF_SIZE(5)] = { 0 };
287 bool skew = false;
288 wnaf::fixed_wnaf<1, 5>(rand_buffer, wnaf, skew, 0);
289 uint64_t recovered_hi = 0;
290 uint64_t recovered_lo = 0;
291 recover_fixed_wnaf(wnaf, skew, recovered_hi, recovered_lo, 5);
292 EXPECT_EQ(rand_buffer[0], recovered_lo);
293 EXPECT_EQ(rand_buffer[1], recovered_hi);
294}
295
296TEST(wnaf, WnafFixedWithEndoSplit)
297{
299 k.data[3] &= 0x0fffffffffffffffUL;
300
301 fr k1{ 0, 0, 0, 0 };
302 fr k2{ 0, 0, 0, 0 };
303
305 uint64_t wnaf[WNAF_SIZE(5)] = { 0 };
306 uint64_t endo_wnaf[WNAF_SIZE(5)] = { 0 };
307 bool skew = false;
308 bool endo_skew = false;
309 wnaf::fixed_wnaf<1, 5>(&k1.data[0], wnaf, skew, 0);
310 wnaf::fixed_wnaf<1, 5>(&k2.data[0], endo_wnaf, endo_skew, 0);
311
312 fr k1_recovered{ 0, 0, 0, 0 };
313 fr k2_recovered{ 0, 0, 0, 0 };
314
315 recover_fixed_wnaf(wnaf, skew, k1_recovered.data[1], k1_recovered.data[0], 5);
316 recover_fixed_wnaf(endo_wnaf, endo_skew, k2_recovered.data[1], k2_recovered.data[0], 5);
317
318 fr result;
319 fr lambda = fr::cube_root_of_unity();
320 result = k2_recovered * lambda;
321 result = k1_recovered - result;
322
323 EXPECT_EQ(result, k);
324}
325
326// NOLINTEND(cppcoreguidelines-avoid-c-arrays)
virtual uint256_t get_random_uint256()=0
numeric::RNG & engine
std::unique_ptr< uint8_t[]> buffer
Definition engine.cpp:60
BoothSliceParams compute_booth_slice_params(size_t bit_offset, size_t window_bits, size_t num_uint64_limbs) noexcept
uint32_t booth_packed_digit(const uint64_t *s, const BoothSliceParams &sp, size_t window_bits) noexcept
Read a (window_bits+1)-bit window from s[] (uint64 limbs) and apply Constantine's signedWindowEncodin...
RNG & get_debug_randomness(bool reset, std::uint_fast64_t seed)
Definition engine.cpp:245
Entry point for Barretenberg command-line interface.
Definition api.hpp:5
TEST(BoomerangMegaCircuitBuilder, BasicCircuit)
constexpr decltype(auto) get(::tuplet::tuple< T... > &&t) noexcept
Definition tuple.hpp:13
unsigned __int128 uint128_t
Definition serialize.hpp:45
static constexpr field cube_root_of_unity()
static void split_into_endomorphism_scalars(const field &k, field &k1, field &k2)
Full-width endomorphism decomposition: k ≡ k1 - k2·λ (mod r). Modifies the field elements k1 and k2.
static field random_element(numeric::RNG *engine=nullptr) noexcept
#define WNAF_SIZE(x)
Definition wnaf.hpp:40