Skip to content

Commit 7cc9939

Browse files
authoredMar 18, 2025··
chore: add a safe blst wrapper (#2223)
* add blst wrapper * add note on points at infinity * g1: - Move is_on_curve check into decode_and_check_g1 method (This should never be skipped) - Opinionated: make the default extract_g1 method always do a subgroup check, so that it is harder to mess things up * g2: - Move is_on_curve check into decode_and_check_g2 method (This should never be skipped) - Opinionated: make the default extract_g2 method always do a subgroup check, so that it is harder to mess things up * g1_add: - refactor to use wrapper blst methods and be explicit about performing no subgroup check * g2_add: - refactor to use wrapper blst methods and be explicit about performing no subgroup check * g1_msm: - refactor to use safe blst wrapper - remove true argument to extract_g1_inputs since subgroup checks are on by default - No longer need to convert to jacobian to affine and then back to affine * g2_msm: - refactor to use safe blst wrapper - remove true argument to extract_g2_inputs since subgroup checks are on by default - No longer need to convert to jacobian to affine and then back to affine * map_fp_to_g1: - use safe blst wrapper * map_fp2_to_g2: - use safe blst wrapper * pairing: - use safe blst wrapper * blst: rename scalars to scalars_bytes and divide by SCALAR_LENGTH when checking against the number of points * g1_msms/g2_msm: scalars -> scalars_bytes * g2_msm: rename variable * modify comment * temp remove checks to show that they are not needed * add back point at infinity check * add inline * fix merge * add inline to all functions * fix merge
1 parent 8a25053 commit 7cc9939

File tree

12 files changed

+417
-187
lines changed

12 files changed

+417
-187
lines changed
 

‎crates/optimism/src/evm.rs

+4-4
Original file line numberDiff line numberDiff line change
@@ -549,8 +549,8 @@ mod tests {
549549
#[test]
550550
#[cfg(feature = "blst")]
551551
fn test_halted_tx_call_bls12_381_pairing_out_of_gas() {
552-
let pairing_gas: u64 = bls12_381_const::PAIRING_PAIRING_MULTIPLIER_BASE
553-
+ bls12_381_const::PAIRING_PAIRING_OFFSET_BASE;
552+
let pairing_gas: u64 =
553+
bls12_381_const::PAIRING_MULTIPLIER_BASE + bls12_381_const::PAIRING_OFFSET_BASE;
554554

555555
let ctx = Context::op()
556556
.modify_tx_chained(|tx| {
@@ -583,8 +583,8 @@ mod tests {
583583
#[test]
584584
#[cfg(feature = "blst")]
585585
fn test_tx_call_bls12_381_pairing_wrong_input_layout() {
586-
let pairing_gas: u64 = bls12_381_const::PAIRING_PAIRING_MULTIPLIER_BASE
587-
+ bls12_381_const::PAIRING_PAIRING_OFFSET_BASE;
586+
let pairing_gas: u64 =
587+
bls12_381_const::PAIRING_MULTIPLIER_BASE + bls12_381_const::PAIRING_OFFSET_BASE;
588588

589589
let ctx = Context::op()
590590
.modify_tx_chained(|tx| {

‎crates/precompile/src/bls12_381.rs

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::PrecompileWithAddress;
22

3+
mod blst;
34
mod g1;
45
pub mod g1_add;
56
pub mod g1_msm;
+265
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
// This module contains a safe wrapper around the blst library.
2+
3+
use crate::bls12_381_const::SCALAR_LENGTH;
4+
use blst::{
5+
blst_final_exp, blst_fp, blst_fp12, blst_fp12_is_one, blst_fp12_mul, blst_fp2, blst_map_to_g1,
6+
blst_map_to_g2, blst_miller_loop, blst_p1, blst_p1_add_or_double_affine, blst_p1_affine,
7+
blst_p1_from_affine, blst_p1_to_affine, blst_p2, blst_p2_add_or_double_affine, blst_p2_affine,
8+
blst_p2_from_affine, blst_p2_to_affine, MultiPoint,
9+
};
10+
11+
#[inline]
12+
fn p1_to_affine(p: &blst_p1) -> blst_p1_affine {
13+
let mut p_affine = blst_p1_affine::default();
14+
// SAFETY: both inputs are valid blst types
15+
unsafe { blst_p1_to_affine(&mut p_affine, p) };
16+
p_affine
17+
}
18+
19+
#[inline]
20+
fn p1_from_affine(p_affine: &blst_p1_affine) -> blst_p1 {
21+
let mut p = blst_p1::default();
22+
// SAFETY: both inputs are valid blst types
23+
unsafe { blst_p1_from_affine(&mut p, p_affine) };
24+
p
25+
}
26+
27+
#[inline]
28+
fn p1_add_or_double(p: &blst_p1, p_affine: &blst_p1_affine) -> blst_p1 {
29+
let mut result = blst_p1::default();
30+
// SAFETY: all inputs are valid blst types
31+
unsafe { blst_p1_add_or_double_affine(&mut result, p, p_affine) };
32+
result
33+
}
34+
35+
#[inline]
36+
fn p2_to_affine(p: &blst_p2) -> blst_p2_affine {
37+
let mut p_affine = blst_p2_affine::default();
38+
// SAFETY: both inputs are valid blst types
39+
unsafe { blst_p2_to_affine(&mut p_affine, p) };
40+
p_affine
41+
}
42+
43+
#[inline]
44+
fn p2_from_affine(p_affine: &blst_p2_affine) -> blst_p2 {
45+
let mut p = blst_p2::default();
46+
// SAFETY: both inputs are valid blst types
47+
unsafe { blst_p2_from_affine(&mut p, p_affine) };
48+
p
49+
}
50+
51+
#[inline]
52+
fn p2_add_or_double(p: &blst_p2, p_affine: &blst_p2_affine) -> blst_p2 {
53+
let mut result = blst_p2::default();
54+
// SAFETY: all inputs are valid blst types
55+
unsafe { blst_p2_add_or_double_affine(&mut result, p, p_affine) };
56+
result
57+
}
58+
59+
/// p1_add_affine adds two G1 points in affine form, returning the result in affine form
60+
///
61+
/// Note: `a` and `b` can be the same, ie this method is safe to call if one wants
62+
/// to essentially double a point
63+
#[inline]
64+
pub(super) fn p1_add_affine(a: &blst_p1_affine, b: &blst_p1_affine) -> blst_p1_affine {
65+
// Convert first point to Jacobian coordinates
66+
let a_jacobian = p1_from_affine(a);
67+
68+
// Add second point (in affine) to first point (in Jacobian)
69+
let sum_jacobian = p1_add_or_double(&a_jacobian, b);
70+
71+
// Convert result back to affine coordinates
72+
p1_to_affine(&sum_jacobian)
73+
}
74+
75+
/// Add two G2 points in affine form, returning the result in affine form
76+
#[inline]
77+
pub(super) fn p2_add_affine(a: &blst_p2_affine, b: &blst_p2_affine) -> blst_p2_affine {
78+
// Convert first point to Jacobian coordinates
79+
let a_jacobian = p2_from_affine(a);
80+
81+
// Add second point (in affine) to first point (in Jacobian)
82+
let sum_jacobian = p2_add_or_double(&a_jacobian, b);
83+
84+
// Convert result back to affine coordinates
85+
p2_to_affine(&sum_jacobian)
86+
}
87+
88+
/// Performs multi-scalar multiplication (MSM) for G1 points
89+
///
90+
/// Takes a vector of G1 points and corresponding scalars, and returns their weighted sum
91+
///
92+
/// Note: This method assumes that `g1_points` does not contain any points at infinity.
93+
#[inline]
94+
pub(super) fn p1_msm(
95+
g1_points: Vec<blst_p1_affine>,
96+
scalars_bytes: Vec<u8>,
97+
nbits: usize,
98+
) -> blst_p1_affine {
99+
assert!(
100+
scalars_bytes.len() % SCALAR_LENGTH == 0,
101+
"Each scalar should be {SCALAR_LENGTH} bytes"
102+
);
103+
104+
assert_eq!(
105+
g1_points.len(),
106+
scalars_bytes.len() / SCALAR_LENGTH,
107+
"number of scalars should equal the number of g1 points"
108+
);
109+
// When no inputs are given, we trigger an assert.
110+
// While it is mathematically sound to have no inputs (can return point at infinity)
111+
// EIP2537 forbids this and since this is the only function that
112+
// currently calls this method, we have this assert.
113+
assert!(
114+
!g1_points.is_empty(),
115+
"number of inputs to pairing should be non-zero"
116+
);
117+
118+
// Perform multi-scalar multiplication
119+
let multiexp = g1_points.mult(&scalars_bytes, nbits);
120+
121+
// Convert result back to affine coordinates
122+
p1_to_affine(&multiexp)
123+
}
124+
125+
/// Performs multi-scalar multiplication (MSM) for G2 points
126+
///
127+
/// Takes a vector of G2 points and corresponding scalars, and returns their weighted sum
128+
///
129+
/// Note: This method assumes that `g2_points` does not contain any points at infinity.
130+
#[inline]
131+
pub(super) fn p2_msm(
132+
g2_points: Vec<blst_p2_affine>,
133+
scalars_bytes: Vec<u8>,
134+
nbits: usize,
135+
) -> blst_p2_affine {
136+
assert!(
137+
scalars_bytes.len() % SCALAR_LENGTH == 0,
138+
"Each scalar should be {SCALAR_LENGTH} bytes"
139+
);
140+
141+
assert_eq!(
142+
g2_points.len(),
143+
scalars_bytes.len() / SCALAR_LENGTH,
144+
"number of scalars should equal the number of g2 points"
145+
);
146+
// When no inputs are given, we trigger an assert.
147+
// While it is mathematically sound to have no inputs (can return point at infinity)
148+
// EIP2537 forbids this and since this is the only function that
149+
// currently calls this method, we have this assert.
150+
assert!(
151+
!g2_points.is_empty(),
152+
"number of inputs to pairing should be non-zero"
153+
);
154+
155+
// Perform multi-scalar multiplication
156+
let multiexp = g2_points.mult(&scalars_bytes, nbits);
157+
158+
// Convert result back to affine coordinates
159+
p2_to_affine(&multiexp)
160+
}
161+
162+
/// Maps a field element to a G1 point
163+
///
164+
/// Takes a field element (blst_fp) and returns the corresponding G1 point in affine form
165+
#[inline]
166+
pub(super) fn map_fp_to_g1(fp: &blst_fp) -> blst_p1_affine {
167+
// Create a new G1 point in Jacobian coordinates
168+
let mut p = blst_p1::default();
169+
170+
// Map the field element to a point on the curve
171+
// SAFETY: `p` and `fp` are blst values
172+
// Third argument is unused if null
173+
unsafe { blst_map_to_g1(&mut p, fp, core::ptr::null()) };
174+
175+
// Convert to affine coordinates
176+
p1_to_affine(&p)
177+
}
178+
179+
/// Maps a field element to a G2 point
180+
///
181+
/// Takes a field element (blst_fp2) and returns the corresponding G2 point in affine form
182+
#[inline]
183+
pub(super) fn map_fp2_to_g2(fp2: &blst_fp2) -> blst_p2_affine {
184+
// Create a new G2 point in Jacobian coordinates
185+
let mut p = blst_p2::default();
186+
187+
// Map the field element to a point on the curve
188+
// SAFETY: `p` and `fp2` are blst values
189+
// Third argument is unused if null
190+
unsafe { blst_map_to_g2(&mut p, fp2, core::ptr::null()) };
191+
192+
// Convert to affine coordinates
193+
p2_to_affine(&p)
194+
}
195+
196+
/// Computes a single miller loop for a given G1, G2 pair
197+
#[inline]
198+
fn compute_miller_loop(g1: &blst_p1_affine, g2: &blst_p2_affine) -> blst_fp12 {
199+
let mut result = blst_fp12::default();
200+
201+
// SAFETY: All arguments are valid blst types
202+
unsafe { blst_miller_loop(&mut result, g2, g1) }
203+
204+
result
205+
}
206+
207+
/// multiply_fp12 multiplies two fp12 elements
208+
#[inline]
209+
fn multiply_fp12(a: &blst_fp12, b: &blst_fp12) -> blst_fp12 {
210+
let mut result = blst_fp12::default();
211+
212+
// SAFETY: All arguments are valid blst types
213+
unsafe { blst_fp12_mul(&mut result, a, b) }
214+
215+
result
216+
}
217+
218+
/// final_exp computes the final exponentiation on an fp12 element
219+
#[inline]
220+
fn final_exp(f: &blst_fp12) -> blst_fp12 {
221+
let mut result = blst_fp12::default();
222+
223+
// SAFETY: All arguments are valid blst types
224+
unsafe { blst_final_exp(&mut result, f) }
225+
226+
result
227+
}
228+
229+
/// is_fp12_one checks if an fp12 element equals
230+
/// multiplicative identity element, one
231+
#[inline]
232+
fn is_fp12_one(f: &blst_fp12) -> bool {
233+
// SAFETY: argument is a valid blst type
234+
unsafe { blst_fp12_is_one(f) }
235+
}
236+
237+
/// pairing_check performs a pairing check on a list of G1 and G2 point pairs and
238+
/// returns true if the result is equal to the identity element.
239+
#[inline]
240+
pub(super) fn pairing_check(pairs: &[(blst_p1_affine, blst_p2_affine)]) -> bool {
241+
// When no inputs are given, we trigger an assert.
242+
// While it is mathematically sound to have no inputs (can return true)
243+
// EIP2537 forbids this and since this is the only function that
244+
// currently calls this method, we have this assert.
245+
assert!(
246+
!pairs.is_empty(),
247+
"number of inputs to pairing should be non-zero"
248+
);
249+
250+
// Compute the miller loop for the first pair
251+
let (first_g1, first_g2) = &pairs[0];
252+
let mut acc = compute_miller_loop(first_g1, first_g2);
253+
254+
// For the remaining pairs, compute miller loop and multiply with the accumulated result
255+
for (g1, g2) in pairs.iter().skip(1) {
256+
let ml = compute_miller_loop(g1, g2);
257+
acc = multiply_fp12(&acc, &ml);
258+
}
259+
260+
// Apply final exponentiation and check if result is 1
261+
let final_result = final_exp(&acc);
262+
263+
// Check if the result is one (identity element)
264+
is_fp12_one(&final_result)
265+
}

‎crates/precompile/src/bls12_381/g1.rs

+35-17
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ pub(super) fn encode_g1_point(input: *const blst_p1_affine) -> Bytes {
1818
/// Returns a `blst_p1_affine` from the provided byte slices, which represent the x and y
1919
/// affine coordinates of the point.
2020
///
21-
/// If the x or y coordinate do not represent a canonical field element, an error is returned.
22-
///
23-
/// See [fp_from_bendian] for more information.
21+
/// - If the x or y coordinate do not represent a canonical field element, an error is returned.
22+
/// See [fp_from_bendian] for more information.
23+
/// - If the point is not on the curve, an error is returned.
2424
pub(super) fn decode_and_check_g1(
2525
p0_x: &[u8; 48],
2626
p0_y: &[u8; 48],
@@ -30,13 +30,44 @@ pub(super) fn decode_and_check_g1(
3030
y: fp_from_bendian(p0_y)?,
3131
};
3232

33+
// From EIP-2537:
34+
//
35+
// Error cases:
36+
//
37+
// * An input is neither a point on the G1 elliptic curve nor the infinity point
38+
//
39+
// SAFETY: Out is a blst value.
40+
if unsafe { !blst_p1_affine_on_curve(&out) } {
41+
return Err(PrecompileError::Other(
42+
"Element not on G1 curve".to_string(),
43+
));
44+
}
45+
3346
Ok(out)
3447
}
3548

49+
/// Extracts a G1 point in Affine format from a 128 byte slice representation.
50+
///
51+
/// Note: By default, subgroup checks are performed.
52+
pub(super) fn extract_g1_input(input: &[u8]) -> Result<blst_p1_affine, PrecompileError> {
53+
_extract_g1_input(input, true)
54+
}
55+
/// Extracts a G1 point in Affine format from a 128 byte slice representation.
56+
/// without performing a subgroup check.
57+
///
58+
/// Note: Skipping subgroup checks can introduce security issues.
59+
/// This method should only be called if:
60+
/// - The EIP specifies that no subgroup check should be performed
61+
/// - One can be certain that the point is in the correct subgroup.
62+
pub(super) fn extract_g1_input_no_subgroup_check(
63+
input: &[u8],
64+
) -> Result<blst_p1_affine, PrecompileError> {
65+
_extract_g1_input(input, false)
66+
}
3667
/// Extracts a G1 point in Affine format from a 128 byte slice representation.
3768
///
3869
/// **Note**: This function will perform a G1 subgroup check if `subgroup_check` is set to `true`.
39-
pub(super) fn extract_g1_input(
70+
fn _extract_g1_input(
4071
input: &[u8],
4172
subgroup_check: bool,
4273
) -> Result<blst_p1_affine, PrecompileError> {
@@ -51,19 +82,6 @@ pub(super) fn extract_g1_input(
5182
let input_p0_y = remove_padding(&input[PADDED_FP_LENGTH..PADDED_G1_LENGTH])?;
5283
let out = decode_and_check_g1(input_p0_x, input_p0_y)?;
5384

54-
// From EIP-2537:
55-
//
56-
// Error cases:
57-
//
58-
// * An input is neither a point on the G1 elliptic curve nor the infinity point
59-
//
60-
// SAFETY: Out is a blst value.
61-
if unsafe { !blst_p1_affine_on_curve(&out) } {
62-
return Err(PrecompileError::Other(
63-
"Element not on G1 curve".to_string(),
64-
));
65-
}
66-
6785
if subgroup_check {
6886
// NB: Subgroup checks
6987
//

0 commit comments

Comments
 (0)