Skip to content

Commit 8374744

Browse files
authored
feat: merge-train/barretenberg (#23455)
See [merge-train-readme.md](https://github.com/AztecProtocol/aztec-packages/blob/next/.github/workflows/merge-train-readme.md). This is a merge-train.
2 parents b36f2f7 + f294d36 commit 8374744

14 files changed

Lines changed: 1506 additions & 13 deletions

barretenberg/cpp/src/barretenberg/bbapi/bbapi_ecc.cpp

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ GrumpkinMul::Response GrumpkinMul::execute(BBApiRequest& request) &&
1111
if (!point.on_curve()) {
1212
BBAPI_ERROR(request, "Input point must be on the curve");
1313
}
14-
return { point * scalar };
14+
return { grumpkin::g1::element(point).mul_const_time(scalar).to_affine_const_time() };
1515
}
1616

1717
GrumpkinAdd::Response GrumpkinAdd::execute(BBApiRequest& request) &&
@@ -32,7 +32,11 @@ GrumpkinBatchMul::Response GrumpkinBatchMul::execute(BBApiRequest& request) &&
3232
BBAPI_ERROR(request, "Input point must be on the curve");
3333
}
3434
}
35-
auto output = grumpkin::g1::element::batch_mul_with_endomorphism(points, scalar);
35+
std::vector<grumpkin::g1::affine_element> output;
36+
output.reserve(points.size());
37+
for (const auto& p : points) {
38+
output.emplace_back(grumpkin::g1::element(p).mul_const_time(scalar).to_affine_const_time());
39+
}
3640
return { std::move(output) };
3741
}
3842

@@ -54,7 +58,7 @@ Secp256k1Mul::Response Secp256k1Mul::execute(BBApiRequest& request) &&
5458
if (!point.on_curve()) {
5559
BBAPI_ERROR(request, "Input point must be on the curve");
5660
}
57-
return { point * scalar };
61+
return { secp256k1::g1::element(point).mul_const_time(scalar).to_affine_const_time() };
5862
}
5963

6064
Secp256k1GetRandomFr::Response Secp256k1GetRandomFr::execute(BB_UNUSED BBApiRequest& request) &&
@@ -87,7 +91,7 @@ Bn254G1Mul::Response Bn254G1Mul::execute(BBApiRequest& request) &&
8791
if (!point.on_curve()) {
8892
BBAPI_ERROR(request, "Input point must be on the curve");
8993
}
90-
auto result = point * scalar;
94+
auto result = bb::g1::element(point).mul_const_time(scalar).to_affine_const_time();
9195
if (!result.on_curve()) {
9296
BBAPI_ERROR(request, "Output point must be on the curve");
9397
}

barretenberg/cpp/src/barretenberg/bbapi/bbapi_ecdsa.cpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ namespace bb::bbapi {
1010
// Secp256k1 implementations
1111
EcdsaSecp256k1ComputePublicKey::Response EcdsaSecp256k1ComputePublicKey::execute(BB_UNUSED BBApiRequest& request) &&
1212
{
13-
return { secp256k1::g1::one * private_key };
13+
return { secp256k1::g1::element(secp256k1::g1::one).mul_const_time(private_key).to_affine_const_time() };
1414
}
1515

1616
EcdsaSecp256k1ConstructSignature::Response EcdsaSecp256k1ConstructSignature::execute(BB_UNUSED BBApiRequest& request) &&
1717
{
18-
auto pub_key = secp256k1::g1::one * private_key;
18+
auto pub_key = secp256k1::g1::element(secp256k1::g1::one).mul_const_time(private_key).to_affine_const_time();
1919
crypto::ecdsa_key_pair<secp256k1::fr, secp256k1::g1> key_pair = { private_key, pub_key };
2020

2121
std::string message_str(reinterpret_cast<const char*>(message.data()), message.size());
@@ -44,12 +44,12 @@ EcdsaSecp256k1VerifySignature::Response EcdsaSecp256k1VerifySignature::execute(B
4444
// Secp256r1 implementations
4545
EcdsaSecp256r1ComputePublicKey::Response EcdsaSecp256r1ComputePublicKey::execute(BB_UNUSED BBApiRequest& request) &&
4646
{
47-
return { secp256r1::g1::one * private_key };
47+
return { secp256r1::g1::element(secp256r1::g1::one).mul_const_time(private_key).to_affine_const_time() };
4848
}
4949

5050
EcdsaSecp256r1ConstructSignature::Response EcdsaSecp256r1ConstructSignature::execute(BB_UNUSED BBApiRequest& request) &&
5151
{
52-
auto pub_key = secp256r1::g1::one * private_key;
52+
auto pub_key = secp256r1::g1::element(secp256r1::g1::one).mul_const_time(private_key).to_affine_const_time();
5353
crypto::ecdsa_key_pair<secp256r1::fr, secp256r1::g1> key_pair = { private_key, pub_key };
5454

5555
std::string message_str(reinterpret_cast<const char*>(message.data()), message.size());

barretenberg/cpp/src/barretenberg/bbapi/bbapi_schnorr.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@ namespace bb::bbapi {
88

99
SchnorrComputePublicKey::Response SchnorrComputePublicKey::execute(BB_UNUSED BBApiRequest& request) &&
1010
{
11-
return { grumpkin::g1::one * private_key };
11+
return { grumpkin::g1::element(grumpkin::g1::one).mul_const_time(private_key).to_affine_const_time() };
1212
}
1313

1414
SchnorrConstructSignature::Response SchnorrConstructSignature::execute(BB_UNUSED BBApiRequest& request) &&
1515
{
16-
grumpkin::g1::affine_element pub_key = grumpkin::g1::one * private_key;
16+
grumpkin::g1::affine_element pub_key =
17+
grumpkin::g1::element(grumpkin::g1::one).mul_const_time(private_key).to_affine_const_time();
1718
crypto::schnorr_key_pair<grumpkin::fr, grumpkin::g1> key_pair = { private_key, pub_key };
1819

1920
auto sig = crypto::schnorr_construct_signature<grumpkin::fr, grumpkin::g1>(message_field, key_pair);

barretenberg/cpp/src/barretenberg/crypto/ecdsa/ecdsa_impl.hpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@ ecdsa_signature ecdsa_construct_signature(const std::string& message, const ecds
3131

3232
// Compute R = k * G. k is the secret RFC6979 nonce, so use the constant-time multiplication
3333
// to defend against the Hamming-weight / bit-length timing leak in operator*.
34-
typename G1::affine_element R(typename G1::element(G1::one).mul_const_time(k));
34+
typename G1::affine_element R(typename G1::element(G1::one).mul_const_time(k).to_affine_const_time());
3535

3636
// Compute the signature
3737
Fr r = Fr(R.x);
38-
Fr s = (z + r * account.private_key) / k;
38+
Fr s = (z + r * account.private_key) * k.invert_const_time();
3939
secure_erase_bytes(&k, sizeof(k));
4040

4141
// Ensure that the value of s is "low", i.e. s := min{ s, (|Fr| - s) }

barretenberg/cpp/src/barretenberg/crypto/schnorr/schnorr.tcc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ schnorr_signature schnorr_construct_signature(const typename G1::Fq& message_fie
8686

8787
// k is a secret nonce; use the constant-time multiplication to defend against the
8888
// Hamming-weight / bit-length timing leak in operator*.
89-
typename G1::affine_element R(typename G1::element(G1::one).mul_const_time(k));
89+
typename G1::affine_element R(typename G1::element(G1::one).mul_const_time(k).to_affine_const_time());
9090

9191
using Fq = typename G1::Fq;
9292
Fq e = schnorr_generate_challenge<G1>(message_field, public_key, R);
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
/**
2+
* @file invert_differential.fuzzer.cpp
3+
* @brief Differential fuzzer for Bernstein-Yang modular inverse vs Fermat (modexp).
4+
*
5+
* Reuses the FieldVM driver from `multi_field.fuzzer.cpp` to generate diverse
6+
* field elements via sequences of arithmetic operations. After each VM phase
7+
* it takes the last element produced (the highest-indexed non-zero slot in the
8+
* VM's internal state, with a fallback to slot 0) and computes its inverse
9+
* three different ways:
10+
*
11+
* - A: `pow(modulus_minus_two)` — Fermat's little theorem (modexp).
12+
* - B: `invert_vartime<Native5x64>` — safegcd, 5×64-bit limb kernel
13+
* (selected on native targets, BATCH=62).
14+
* - C: `invert_vartime<Wasm9x29>` — safegcd, 9×29-bit limb kernel
15+
* (selected on WASM targets, BATCH=58).
16+
*
17+
* All three are compared in canonical (non-Montgomery) form. Any discrepancy
18+
* triggers an abort with full diagnostic output (field type, input, all three
19+
* outputs, plus Montgomery checks `a * X ?= 1` for each). Cross-checking the
20+
* WASM kernel here gives it libFuzzer coverage even though libFuzzer itself
21+
* doesn't run under WASM — both kernels are plain C++ classes.
22+
*
23+
* Only 254-bit primes are tested (BN254 Fr/Fq, Grumpkin shares the BN254
24+
* curves), since the 5-limb signed BY state requires p < 2^255 and the
25+
* production `field::invert()` dispatch also gates on this. 256-bit primes
26+
* (secp256k1/r1) don't use BY and are skipped.
27+
*/
28+
29+
#include "barretenberg/ecc/curves/bn254/fq.hpp"
30+
#include "barretenberg/ecc/curves/bn254/fr.hpp"
31+
#include "barretenberg/ecc/fields/bernstein_yang_inverse.hpp"
32+
#include "barretenberg/ecc/fields/field.fuzzer.hpp"
33+
#include <cassert>
34+
#include <cstddef>
35+
#include <cstdio>
36+
#include <cstdlib>
37+
#include <cstring>
38+
#include <vector>
39+
40+
using namespace bb;
41+
using numeric::uint256_t;
42+
43+
// ---------------------------------------------------------------
44+
// Phase header — same 2-byte layout as multi_field.fuzzer.cpp but restricted
45+
// to the two 254-bit fields that actually use BY.
46+
// ---------------------------------------------------------------
47+
enum class FieldType : uint8_t {
48+
BN254_FQ = 0,
49+
BN254_FR = 1,
50+
};
51+
52+
static constexpr size_t NUM_FIELD_TYPES = 2;
53+
static constexpr size_t MAX_STEPS = 64;
54+
static constexpr size_t PHASE_HEADER_SIZE = 2;
55+
56+
struct VMPhaseHeader {
57+
uint8_t field_type;
58+
uint8_t steps;
59+
};
60+
static_assert(sizeof(VMPhaseHeader) == 2, "VMPhaseHeader must be 2 bytes");
61+
62+
template <typename Field> static uint256_t reduce_to_modulus(const uint256_t& value)
63+
{
64+
return (value < Field::modulus) ? value : (value % Field::modulus);
65+
}
66+
67+
template <typename Field>
68+
static void import_state_with_reduction(FieldVM<Field>& vm, const std::vector<uint256_t>& state)
69+
{
70+
for (size_t i = 0; i < INTERNAL_STATE_SIZE && i < state.size(); i++) {
71+
vm.uint_internal_state[i] = reduce_to_modulus<Field>(state[i]);
72+
vm.field_internal_state[i] = Field(vm.uint_internal_state[i]);
73+
}
74+
}
75+
76+
// ---------------------------------------------------------------
77+
// Differential oracle.
78+
//
79+
// Fetches `a_raw` (the non-Montgomery integer) from the VM's uint state and
80+
// computes a^{-1} two ways; aborts on mismatch.
81+
// ---------------------------------------------------------------
82+
template <typename Field> static Field raw_to_montgomery(const uint256_t& raw)
83+
{
84+
Field f{ raw.data[0], raw.data[1], raw.data[2], raw.data[3] };
85+
f.self_to_montgomery_form();
86+
return f;
87+
}
88+
89+
static void print_limbs(const char* label, const uint256_t& v)
90+
{
91+
std::fprintf(stderr,
92+
" %s = 0x%016lx%016lx%016lx%016lx\n",
93+
label,
94+
(unsigned long)v.data[3],
95+
(unsigned long)v.data[2],
96+
(unsigned long)v.data[1],
97+
(unsigned long)v.data[0]);
98+
}
99+
100+
template <typename Field> static void differential_check_inverse(const Field& a_mont, const uint256_t& a_raw)
101+
{
102+
if (a_raw == 0) {
103+
return; // 0 has no inverse — skip.
104+
}
105+
106+
// A: Fermat via pow. We bypass field::invert() (which now dispatches into
107+
// BY) by calling pow(modulus_minus_two) directly, so the paths are
108+
// genuinely independent implementations.
109+
Field fermat_inv = a_mont.pow(Field::modulus_minus_two);
110+
111+
// B, C: Bernstein-Yang safegcd, called with the raw (non-Montgomery) value
112+
// on both the Native5x64 (BATCH=62) and Wasm9x29 (BATCH=58) kernels.
113+
// Each kernel needs its own p_inv_mod_2^BATCH constant.
114+
constexpr uint256_t p_uint = Field::modulus;
115+
constexpr uint64_t p_inv_native =
116+
bernstein_yang::Native5x64::p_inv_mod_2k_from_montgomery_r_inv(Field::Params::r_inv);
117+
constexpr uint64_t p_inv_wasm = bernstein_yang::Wasm9x29::p_inv_mod_2k_from_montgomery_r_inv(Field::Params::r_inv);
118+
119+
uint256_t native_inv_raw = bernstein_yang::invert_vartime<bernstein_yang::Native5x64>(a_raw, p_uint, p_inv_native);
120+
uint256_t wasm_inv_raw = bernstein_yang::invert_vartime<bernstein_yang::Wasm9x29>(a_raw, p_uint, p_inv_wasm);
121+
122+
Field native_inv = raw_to_montgomery<Field>(native_inv_raw);
123+
Field wasm_inv = raw_to_montgomery<Field>(wasm_inv_raw);
124+
125+
const bool native_ok = (fermat_inv == native_inv);
126+
const bool wasm_ok = (fermat_inv == wasm_inv);
127+
if (native_ok && wasm_ok) {
128+
return;
129+
}
130+
131+
std::fprintf(stderr, "\n[invert_differential.fuzzer] MISMATCH\n");
132+
std::fprintf(stderr, " field: %s\n", typeid(Field).name());
133+
std::fprintf(stderr, " native_ok: %s\n", native_ok ? "yes" : "NO");
134+
std::fprintf(stderr, " wasm_ok: %s\n", wasm_ok ? "yes" : "NO");
135+
print_limbs("a_raw ", a_raw);
136+
print_limbs("fermat ", static_cast<uint256_t>(fermat_inv));
137+
print_limbs("BY native ", static_cast<uint256_t>(native_inv));
138+
print_limbs("BY wasm ", static_cast<uint256_t>(wasm_inv));
139+
print_limbs("a*fermat ", static_cast<uint256_t>(a_mont * fermat_inv));
140+
print_limbs("a*native ", static_cast<uint256_t>(a_mont * native_inv));
141+
print_limbs("a*wasm ", static_cast<uint256_t>(a_mont * wasm_inv));
142+
std::fflush(stderr);
143+
std::abort();
144+
}
145+
146+
// Pick the last element produced: highest-indexed non-zero slot of the VM's
147+
// uint state, with a fallback to slot 0 if all slots are zero.
148+
static size_t last_element_index(const std::vector<uint256_t>& state)
149+
{
150+
for (size_t i = state.size(); i > 0; --i) {
151+
if (state[i - 1] != uint256_t(0)) {
152+
return i - 1;
153+
}
154+
}
155+
return 0;
156+
}
157+
158+
template <typename Field>
159+
static int run_phase_and_diff(const VMPhaseHeader& header,
160+
const unsigned char* data,
161+
size_t size,
162+
size_t& data_offset,
163+
std::vector<uint256_t>& current_state)
164+
{
165+
FieldVM<Field> vm(false, header.steps);
166+
if (!current_state.empty()) {
167+
import_state_with_reduction<Field>(vm, current_state);
168+
}
169+
vm.set_max_steps(header.steps);
170+
size_t bytes_consumed = vm.run(data + data_offset, size - data_offset, true);
171+
if (bytes_consumed == 0) {
172+
return 0;
173+
}
174+
175+
if (!vm.check_internal_state()) {
176+
// Internal VM invariant violation — not the inverse bug we're looking
177+
// for, but still a failure of the driver. Report and stop.
178+
std::fprintf(stderr, "[invert_differential.fuzzer] VM internal state check failed\n");
179+
return -1;
180+
}
181+
182+
// Differential inverse check on the last element produced this phase,
183+
// plus every non-zero slot in the final state for extra coverage.
184+
auto uint_state = vm.export_uint_state();
185+
size_t last_idx = last_element_index(uint_state);
186+
differential_check_inverse<Field>(vm.field_internal_state[last_idx], uint_state[last_idx]);
187+
188+
// Extra coverage: also diff every other non-zero slot. Same check on
189+
// many more values per phase, virtually free CPU-wise.
190+
for (size_t i = 0; i < uint_state.size(); ++i) {
191+
if (i != last_idx && uint_state[i] != uint256_t(0)) {
192+
differential_check_inverse<Field>(vm.field_internal_state[i], uint_state[i]);
193+
}
194+
}
195+
196+
current_state = uint_state;
197+
data_offset += bytes_consumed;
198+
return 1;
199+
}
200+
201+
extern "C" int LLVMFuzzerTestOneInput(const unsigned char* data, size_t size)
202+
{
203+
if (size < PHASE_HEADER_SIZE) {
204+
return 0;
205+
}
206+
207+
std::vector<uint256_t> current_state;
208+
size_t data_offset = 0;
209+
210+
while (data_offset + PHASE_HEADER_SIZE <= size) {
211+
const VMPhaseHeader* header_ptr = reinterpret_cast<const VMPhaseHeader*>(data + data_offset);
212+
VMPhaseHeader header = *header_ptr;
213+
214+
FieldType selected_field_type = static_cast<FieldType>(header.field_type % NUM_FIELD_TYPES);
215+
uint8_t selected_steps = header.steps % MAX_STEPS;
216+
if (selected_steps == 0) {
217+
selected_steps = 1;
218+
}
219+
header.field_type = static_cast<uint8_t>(selected_field_type);
220+
header.steps = selected_steps;
221+
222+
int r = 0;
223+
switch (selected_field_type) {
224+
case FieldType::BN254_FQ:
225+
r = run_phase_and_diff<fq>(header, data, size, data_offset, current_state);
226+
break;
227+
case FieldType::BN254_FR:
228+
r = run_phase_and_diff<fr>(header, data, size, data_offset, current_state);
229+
break;
230+
}
231+
232+
if (r < 0) {
233+
return 1;
234+
}
235+
if (r == 0) {
236+
break;
237+
}
238+
}
239+
return 0;
240+
}

0 commit comments

Comments
 (0)