Skip to content

perf(levm): improve ecpairing (bn128) #4130

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Aug 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Perf

### 2025-08-22

- Improve ecpairing (bn128) precompile [#4130](https://github.com/lambdaclass/ethrex/pull/4130)

### 2025-08-20

- Improve blobbasefee opcode [#4092](https://github.com/lambdaclass/ethrex/pull/4092)
Expand Down
116 changes: 52 additions & 64 deletions crates/vm/levm/src/precompiles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -606,9 +606,10 @@ type FirstPointCoordinates = (
);

/// Parses first point coordinates and makes verification of invalid infinite
fn parse_first_point_coordinates(input_data: &[u8]) -> Result<FirstPointCoordinates, VMError> {
let first_point_x = input_data.get(..32).ok_or(InternalError::Slicing)?;
let first_point_y = input_data.get(32..64).ok_or(InternalError::Slicing)?;
#[inline]
fn parse_first_point_coordinates(input_data: &[u8; 192]) -> Result<FirstPointCoordinates, VMError> {
let first_point_x = &input_data[..32];
let first_point_y = &input_data[32..64];

// Infinite is defined by (0,0). Any other zero-combination is invalid
if (u256_from_big_endian(first_point_x) == U256::zero())
Expand All @@ -626,17 +627,19 @@ fn parse_first_point_coordinates(input_data: &[u8]) -> Result<FirstPointCoordina
}

/// Parses second point coordinates and makes verification of invalid infinite and curve belonging.
///
/// Slice must have len of 192. This function is only called from ecpairing which ensures that.
fn parse_second_point_coordinates(
input_data: &[u8],
input_data: &[u8; 192],
) -> Result<
(
FieldElement<Degree2ExtensionField>,
FieldElement<Degree2ExtensionField>,
),
VMError,
> {
let second_point_x_first_part = input_data.get(96..128).ok_or(InternalError::Slicing)?;
let second_point_x_second_part = input_data.get(64..96).ok_or(InternalError::Slicing)?;
let second_point_x_first_part = &input_data[96..128];
let second_point_x_second_part = &input_data[64..96];

// Infinite is defined by (0,0). Any other zero-combination is invalid
if (u256_from_big_endian(second_point_x_first_part) == U256::zero())
Expand All @@ -645,8 +648,8 @@ fn parse_second_point_coordinates(
return Err(PrecompileError::InvalidPoint.into());
}

let second_point_y_first_part = input_data.get(160..192).ok_or(InternalError::Slicing)?;
let second_point_y_second_part = input_data.get(128..160).ok_or(InternalError::Slicing)?;
let second_point_y_first_part = &input_data[160..192];
let second_point_y_second_part = &input_data[128..160];

// Infinite is defined by (0,0). Any other zero-combination is invalid
if (u256_from_big_endian(second_point_y_first_part) == U256::zero())
Expand Down Expand Up @@ -676,16 +679,23 @@ fn parse_second_point_coordinates(
}

/// Handles pairing given a certain elements, and depending on if elements represent infinity, then
/// continues, verifies errors on the other point or calculates the pairing
fn handle_pairing_from_coordinates(
/// verifies errors on the other point returning None or returns the pairing
#[inline(always)] // called only from one place, so inlining always wont increase code size.
#[expect(clippy::type_complexity)]
fn validate_pairing(
first_point_x: FieldElement<MontgomeryBackendPrimeField<BN254FieldModulus, 4>>,
first_point_y: FieldElement<MontgomeryBackendPrimeField<BN254FieldModulus, 4>>,
second_point_x: FieldElement<Degree2ExtensionField>,
second_point_y: FieldElement<Degree2ExtensionField>,
mul: &mut FieldElement<Degree12ExtensionField>,
) -> Result<bool, VMError> {
let zero_element = BN254FieldElement::from(0);
let twcurve_zero_element = BN254TwistCurveFieldElement::from(0);
) -> Result<
Option<(
ShortWeierstrassProjectivePoint<BN254Curve>,
ShortWeierstrassProjectivePoint<BN254TwistCurve>,
)>,
VMError,
> {
let zero_element = BN254FieldElement::zero();
let twcurve_zero_element = BN254TwistCurveFieldElement::zero();
let first_point_is_infinity =
first_point_x.eq(&zero_element) && first_point_y.eq(&zero_element);
let second_point_is_infinity =
Expand All @@ -694,26 +704,23 @@ fn handle_pairing_from_coordinates(
match (first_point_is_infinity, second_point_is_infinity) {
(true, true) => {
// If both points are infinity, then continue to the next input
Ok(true)
Ok(None)
}
(true, false) => {
// If the first point is infinity, then do the checks for the second
let p2 = BN254TwistCurve::create_point_from_affine(
second_point_x.clone(),
second_point_y.clone(),
)
.map_err(|_| PrecompileError::InvalidPoint)?;
let p2 = BN254TwistCurve::create_point_from_affine(second_point_x, second_point_y)
.map_err(|_| PrecompileError::InvalidPoint)?;

if !p2.is_in_subgroup() {
return Err(PrecompileError::PointNotInSubgroup.into());
}
Ok(true)
Ok(None)
}
(false, true) => {
// If the second point is infinity, then do the checks for the first
BN254Curve::create_point_from_affine(first_point_x.clone(), first_point_y.clone())
BN254Curve::create_point_from_affine(first_point_x, first_point_y)
.map_err(|_| PrecompileError::InvalidPoint)?;
Ok(true)
Ok(None)
}
(false, false) => {
// Define the pairing points
Expand All @@ -726,10 +733,7 @@ fn handle_pairing_from_coordinates(
if !second_point.is_in_subgroup() {
return Err(PrecompileError::PointNotInSubgroup.into());
}

// Get the result of the pairing and affect the mul value with it
update_pairing_result(mul, first_point, second_point)?;
Ok(false)
Ok(Some((first_point, second_point)))
}
}
}
Expand All @@ -747,58 +751,42 @@ pub fn ecpairing(calldata: &Bytes, gas_remaining: &mut u64) -> Result<Bytes, VME
let gas_cost = gas_cost::ecpairing(inputs_amount)?;
increase_precompile_consumed_gas(gas_cost, gas_remaining)?;

let mut valid_pairs = Vec::new();
let mut mul: FieldElement<Degree12ExtensionField> = QuadraticExtensionFieldElement::one();
for input_index in 0..inputs_amount {
// Define the input indexes and slice calldata to get the input data
let input_start = input_index
.checked_mul(192)
.ok_or(InternalError::Overflow)?;
let input_end = input_start
.checked_add(192)
.ok_or(InternalError::Overflow)?;

let input_data = calldata
.get(input_start..input_end)
.ok_or(InternalError::Slicing)?;
for input in calldata.chunks_exact(192) {
#[expect(unsafe_code, reason = "chunks_exact ensures the conversion is valid")]
let input: [u8; 192] = unsafe { input.try_into().unwrap_unchecked() };

let (first_point_x, first_point_y) = parse_first_point_coordinates(input_data)?;
let (first_point_x, first_point_y) = parse_first_point_coordinates(&input)?;

let (second_point_x, second_point_y) = parse_second_point_coordinates(input_data)?;
let (second_point_x, second_point_y) = parse_second_point_coordinates(&input)?;

if handle_pairing_from_coordinates(
first_point_x,
first_point_y,
second_point_x,
second_point_y,
&mut mul,
)? {
continue;
if let Some(pair) =
validate_pairing(first_point_x, first_point_y, second_point_x, second_point_y)?
{
valid_pairs.push(pair);
}
}

#[allow(
clippy::arithmetic_side_effects,
reason = "library will not panic on mul overflow"
)]
if !valid_pairs.is_empty() {
let batch: Vec<_> = valid_pairs.iter().map(|(p1, p2)| (p1, p2)).collect();
let pairing_result = BN254AtePairing::compute_batch(&batch)
.map_err(|_| PrecompileError::BN254AtePairingError)?;
Comment on lines +777 to +779
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sent an issue to Lambdaworks #1019. This allocation here seems avoidable.

mul *= pairing_result;
}

// Generate the result from the variable mul
let success = mul.eq(&QuadraticExtensionFieldElement::one());
let mut result = [0; 32];
result[31] = u8::from(success);
Ok(Bytes::from(result.to_vec()))
}

/// Updates the success variable with the pairing result. I allow this clippy alert because lib handles
/// mul for the type and will not panic in case of overflow
#[allow(clippy::arithmetic_side_effects)]
fn update_pairing_result(
mul: &mut FieldElement<Degree12ExtensionField>,
first_point: ShortWeierstrassProjectivePoint<BN254Curve>,
second_point: ShortWeierstrassProjectivePoint<BN254TwistCurve>,
) -> Result<(), VMError> {
let pairing_result = BN254AtePairing::compute_batch(&[(&first_point, &second_point)])
.map_err(|_| PrecompileError::BN254AtePairingError)?;

*mul *= pairing_result;

Ok(())
}

/// Returns the result of Blake2 hashing algorithm given a certain parameters from the calldata.
pub fn blake2f(calldata: &Bytes, gas_remaining: &mut u64) -> Result<Bytes, VMError> {
if calldata.len() != 213 {
Expand Down