Skip to content

Commit

Permalink
feat(levm): implement precompile ecpairing (#1560)
Browse files Browse the repository at this point in the history
**Motivation**

The goal is to implement the ecpairing precompile.

**Description**

I put a clippy allow for a *= in a math type because it is not handled
like conventional multiplication (it has its own implementation).
The implementation makes pass all EF tests related.

---------

Co-authored-by: ilitteri <[email protected]>
  • Loading branch information
maximopalopoli and ilitteri authored Dec 26, 2024
1 parent d79fa91 commit 9ae9da9
Show file tree
Hide file tree
Showing 3 changed files with 221 additions and 13 deletions.
14 changes: 14 additions & 0 deletions crates/vm/levm/src/gas_cost.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,9 @@ pub const MODEXP_DYNAMIC_QUOTIENT: u64 = 3;
pub const ECADD_COST: u64 = 150;
pub const ECMUL_COST: u64 = 6000;

pub const ECPAIRING_BASE_COST: u64 = 45000;
pub const ECPAIRING_GROUP_COST: u64 = 34000;

pub fn exp(exponent: U256) -> Result<u64, VMError> {
let exponent_byte_size = (exponent
.bits()
Expand Down Expand Up @@ -892,6 +895,17 @@ fn precompile(data_size: usize, static_cost: u64, dynamic_base: u64) -> Result<u
.ok_or(OutOfGasError::GasCostOverflow)?)
}

pub fn ecpairing(groups_number: usize) -> Result<u64, VMError> {
let groups_number = u64::try_from(groups_number).map_err(|_| InternalError::ConversionError)?;

let groups_cost = groups_number
.checked_mul(ECPAIRING_GROUP_COST)
.ok_or(OutOfGasError::GasCostOverflow)?;
groups_cost
.checked_add(ECPAIRING_BASE_COST)
.ok_or(VMError::OutOfGas(OutOfGasError::GasCostOverflow))
}

/// Max message call gas is all but one 64th of the remaining gas in the current context.
/// https://eips.ethereum.org/EIPS/eip-150
pub fn max_message_call_gas(current_call_frame: &CallFrame) -> Result<u64, VMError> {
Expand Down
191 changes: 182 additions & 9 deletions crates/vm/levm/src/precompiles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,18 @@ use keccak_hash::keccak256;
use lambdaworks_math::{
cyclic_group::IsGroup,
elliptic_curve::{
short_weierstrass::curves::bn_254::curve::{BN254Curve, BN254FieldElement},
traits::IsEllipticCurve,
short_weierstrass::{
curves::bn_254::{
curve::{BN254Curve, BN254FieldElement, BN254TwistCurveFieldElement},
field_extension::Degree12ExtensionField,
pairing::BN254AtePairing,
twist::BN254TwistCurve,
},
point::ShortWeierstrassProjectivePoint,
},
traits::{IsEllipticCurve, IsPairing},
},
field::{element::FieldElement, extensions::quadratic::QuadraticExtensionFieldElement},
traits::ByteConversion,
unsigned_integer::element,
};
Expand Down Expand Up @@ -372,9 +381,7 @@ pub fn ecadd(
) -> Result<Bytes, VMError> {
// If calldata does not reach the required length, we should fill the rest with zeros
let calldata = fill_with_zeros(calldata, 128)?;

increase_precompile_consumed_gas(gas_for_call, ECADD_COST, consumed_gas)?;

let first_point_x = calldata
.get(0..32)
.ok_or(PrecompileError::ParsingInputError)?;
Expand Down Expand Up @@ -501,12 +508,178 @@ pub fn ecmul(
}
}

fn ecpairing(
_calldata: &Bytes,
_gas_for_call: u64,
_consumed_gas: &mut u64,
pub fn ecpairing(
calldata: &Bytes,
gas_for_call: u64,
consumed_gas: &mut u64,
) -> Result<Bytes, VMError> {
Ok(Bytes::new())
// The input must always be a multiple of 192 (6 32-byte values)
if calldata.len() % 192 != 0 {
return Err(VMError::PrecompileError(PrecompileError::ParsingInputError));
}

let inputs_amount = calldata.len() / 192;

// Consume gas
let gas_cost = gas_cost::ecpairing(inputs_amount)?;
increase_precompile_consumed_gas(gas_for_call, gas_cost, consumed_gas)?;

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::ArithmeticOperationOverflow)?;
let input_end = input_start
.checked_add(192)
.ok_or(InternalError::ArithmeticOperationOverflow)?;

let input_data = calldata
.get(input_start..input_end)
.ok_or(InternalError::SlicingError)?;

let first_point_x = input_data.get(..32).ok_or(InternalError::SlicingError)?;
let first_point_y = input_data.get(32..64).ok_or(InternalError::SlicingError)?;

// Infinite is defined by (0,0). Any other zero-combination is invalid
if (U256::from_big_endian(first_point_x) == U256::zero())
^ (U256::from_big_endian(first_point_y) == U256::zero())
{
return Err(VMError::PrecompileError(PrecompileError::DefaultError));
}

let first_point_y = BN254FieldElement::from_bytes_be(first_point_y)
.map_err(|_| PrecompileError::DefaultError)?;
let first_point_x = BN254FieldElement::from_bytes_be(first_point_x)
.map_err(|_| PrecompileError::DefaultError)?;

let second_point_x_first_part =
input_data.get(96..128).ok_or(InternalError::SlicingError)?;
let second_point_x_second_part =
input_data.get(64..96).ok_or(InternalError::SlicingError)?;

// Infinite is defined by (0,0). Any other zero-combination is invalid
if (U256::from_big_endian(second_point_x_first_part) == U256::zero())
^ (U256::from_big_endian(second_point_x_second_part) == U256::zero())
{
return Err(VMError::PrecompileError(PrecompileError::DefaultError));
}

let second_point_y_first_part = input_data
.get(160..192)
.ok_or(InternalError::SlicingError)?;
let second_point_y_second_part = input_data
.get(128..160)
.ok_or(InternalError::SlicingError)?;

// Infinite is defined by (0,0). Any other zero-combination is invalid
if (U256::from_big_endian(second_point_y_first_part) == U256::zero())
^ (U256::from_big_endian(second_point_y_second_part) == U256::zero())
{
return Err(VMError::PrecompileError(PrecompileError::DefaultError));
}

let alt_bn128_prime = U256::from_str_radix(
"30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47",
16,
)
.map_err(|_| InternalError::ConversionError)?;

// Check if the second point belongs to the curve (this happens if it's lower than the prime)
if U256::from_big_endian(second_point_x_first_part) >= alt_bn128_prime
|| U256::from_big_endian(second_point_x_second_part) >= alt_bn128_prime
|| U256::from_big_endian(second_point_y_first_part) >= alt_bn128_prime
|| U256::from_big_endian(second_point_y_second_part) >= alt_bn128_prime
{
return Err(VMError::PrecompileError(PrecompileError::DefaultError));
}

let second_point_x_bytes = [second_point_x_first_part, second_point_x_second_part].concat();
let second_point_y_bytes = [second_point_y_first_part, second_point_y_second_part].concat();

let second_point_x: FieldElement<lambdaworks_math::elliptic_curve::short_weierstrass::curves::bn_254::field_extension::Degree2ExtensionField> = BN254TwistCurveFieldElement::from_bytes_be(&second_point_x_bytes)
.map_err(|_| PrecompileError::DefaultError)?;
let second_point_y = BN254TwistCurveFieldElement::from_bytes_be(&second_point_y_bytes)
.map_err(|_| PrecompileError::DefaultError)?;

let zero_element = BN254FieldElement::from(0);
let twcurve_zero_element = BN254TwistCurveFieldElement::from(0);
let first_point_is_infinity =
first_point_x.eq(&zero_element) && first_point_y.eq(&zero_element);
let second_point_is_infinity =
second_point_x.eq(&twcurve_zero_element) && second_point_y.eq(&twcurve_zero_element);

match (first_point_is_infinity, second_point_is_infinity) {
(true, true) => {
// If both points are infinity, then continue to the next input
continue;
}
(true, false) => {
// If the first point is infinity, then do the checks for the second
if let Ok(p2) = BN254TwistCurve::create_point_from_affine(
second_point_x.clone(),
second_point_y.clone(),
) {
if !p2.is_in_subgroup() {
return Err(VMError::PrecompileError(PrecompileError::DefaultError));
} else {
continue;
}
} else {
return Err(VMError::PrecompileError(PrecompileError::DefaultError));
}
}
(false, true) => {
// If the second point is infinity, then do the checks for the first
if BN254Curve::create_point_from_affine(
first_point_x.clone(),
first_point_y.clone(),
)
.is_err()
{
return Err(VMError::PrecompileError(PrecompileError::DefaultError));
}
continue;
}
(false, false) => {
// Define the pairing points
let first_point =
BN254Curve::create_point_from_affine(first_point_x, first_point_y)
.map_err(|_| PrecompileError::DefaultError)?;

let second_point =
BN254TwistCurve::create_point_from_affine(second_point_x, second_point_y)
.map_err(|_| PrecompileError::DefaultError)?;
if !second_point.is_in_subgroup() {
return Err(VMError::PrecompileError(PrecompileError::DefaultError));
}

// Get the result of the pairing and affect the mul value with it
update_pairing_result(&mut mul, first_point, second_point)?;
}
}
}

// 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()))
}

/// 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::DefaultError)?;

*mul *= pairing_result;

Ok(())
}

fn blake2f(
Expand Down
29 changes: 25 additions & 4 deletions crates/vm/levm/tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ use ethrex_levm::{
db::{cache, CacheDB, Db},
errors::{OutOfGasError, TxResult, VMError},
gas_cost::{
self, ECADD_COST, ECMUL_COST, ECRECOVER_COST, IDENTITY_DYNAMIC_BASE, IDENTITY_STATIC_COST,
MODEXP_STATIC_COST, RIPEMD_160_DYNAMIC_BASE, RIPEMD_160_STATIC_COST, SHA2_256_DYNAMIC_BASE,
SHA2_256_STATIC_COST,
self, ECADD_COST, ECMUL_COST, ECPAIRING_BASE_COST, ECPAIRING_GROUP_COST, ECRECOVER_COST,
IDENTITY_DYNAMIC_BASE, IDENTITY_STATIC_COST, MODEXP_STATIC_COST, RIPEMD_160_DYNAMIC_BASE,
RIPEMD_160_STATIC_COST, SHA2_256_DYNAMIC_BASE, SHA2_256_STATIC_COST,
},
memory,
operations::Operation,
precompiles::{ecadd, ecmul, ecrecover, identity, modexp, ripemd_160, sha2_256},
precompiles::{ecadd, ecmul, ecpairing, ecrecover, identity, modexp, ripemd_160, sha2_256},
utils::{new_vm_with_ops, new_vm_with_ops_addr_bal_db, new_vm_with_ops_db, ops_to_bytecode},
vm::{word_to_address, Storage, VM},
Environment,
Expand Down Expand Up @@ -4622,3 +4622,24 @@ fn ecmul_test_2() {
assert_eq!(result, expected_result);
assert_eq!(consumed_gas, ECMUL_COST);
}

#[test]
fn ecpairing_test() {
// This tests a normal behavior, that should return a success (1).
// Basically is passing a set of points that pairs correctly.
let calldata = hex::decode("2cf44499d5d27bb186308b7af7af02ac5bc9eeb6a3d147c186b21fb1b76e18da2c0f001f52110ccfe69108924926e45f0b0c868df0e7bde1fe16d3242dc715f61fb19bb476f6b9e44e2a32234da8212f61cd63919354bc06aef31e3cfaff3ebc22606845ff186793914e03e21df544c34ffe2f2f3504de8a79d9159eca2d98d92bd368e28381e8eccb5fa81fc26cf3f048eea9abfdd85d7ed3ab3698d63e4f902fe02e47887507adf0ff1743cbac6ba291e66f59be6bd763950bb16041a0a85e000000000000000000000000000000000000000000000000000000000000000130644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd451971ff0471b09fa93caaf13cbf443c1aede09cc4328f5a62aad45f40ec133eb4091058a3141822985733cbdddfed0fd8d6c104e9e9eff40bf5abfef9ab163bc72a23af9a5ce2ba2796c1f4e453a370eb0af8c212d9dc9acd8fc02c2e907baea223a8eb0b0996252cb548a4487da97b02422ebc0e834613f954de6c7e0afdc1fc").unwrap();
let calldata = Bytes::from(calldata);

let mut consumed_gas = 0;
let result = ecpairing(&calldata, 10000000, &mut consumed_gas).unwrap();

let expected_result = Bytes::from(
hex::decode("0000000000000000000000000000000000000000000000000000000000000001").unwrap(),
);

assert_eq!(result, expected_result);
assert_eq!(
consumed_gas,
(ECPAIRING_BASE_COST + ECPAIRING_GROUP_COST * 2)
);
}

0 comments on commit 9ae9da9

Please sign in to comment.