From 65d846badc7354c30f75d951d6cb59324a2fa041 Mon Sep 17 00:00:00 2001 From: Maximo Palopoli <96491141+maximopalopoli@users.noreply.github.com> Date: Mon, 23 Dec 2024 10:11:20 -0300 Subject: [PATCH] feat(levm): implement precompile ecadd (#1533) **Motivation** The goal is to implement `ecadd` precompile. **Description** The EF tests related to this functionality are passing --------- Co-authored-by: ilitteri --- Cargo.lock | 12 +++ crates/vm/levm/Cargo.toml | 1 + crates/vm/levm/src/gas_cost.rs | 4 + crates/vm/levm/src/precompiles.rs | 147 +++++++++++++++++++++++++++++- crates/vm/levm/tests/tests.rs | 48 +++++++++- 5 files changed, 204 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a2417a7c4..54d4f7ef3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2190,6 +2190,7 @@ dependencies = [ "ethrex-rlp", "hex", "keccak-hash 0.11.0", + "lambdaworks-math", "libsecp256k1", "num-bigint 0.4.6", "ripemd", @@ -3534,6 +3535,17 @@ dependencies = [ "tiny-keccak", ] +[[package]] +name = "lambdaworks-math" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "708d148956bcdc21ae5c432b4e20bbaa26fd68d5376a3a6c461f41095abea0ba" +dependencies = [ + "rayon", + "serde", + "serde_json", +] + [[package]] name = "lazy-regex" version = "3.3.0" diff --git a/crates/vm/levm/Cargo.toml b/crates/vm/levm/Cargo.toml index cb4ad6571..bb3036458 100644 --- a/crates/vm/levm/Cargo.toml +++ b/crates/vm/levm/Cargo.toml @@ -19,6 +19,7 @@ libsecp256k1 = "0.7.1" sha2 = "0.10.8" ripemd = "0.1.3" num-bigint = "0.4.5" +lambdaworks-math = "0.11.0" [dev-dependencies] hex = "0.4.3" diff --git a/crates/vm/levm/src/gas_cost.rs b/crates/vm/levm/src/gas_cost.rs index 7faadcae0..5fde6a53a 100644 --- a/crates/vm/levm/src/gas_cost.rs +++ b/crates/vm/levm/src/gas_cost.rs @@ -178,8 +178,12 @@ pub const IDENTITY_STATIC_COST: u64 = 15; pub const IDENTITY_DYNAMIC_BASE: u64 = 3; pub const MODEXP_STATIC_COST: u64 = 200; +pub const MODEXP_DYNAMIC_BASE: u64 = 200; pub const MODEXP_DYNAMIC_QUOTIENT: u64 = 3; +pub const ECADD_COST: u64 = 150; +pub const ECMUL_COST: u64 = 6000; + pub fn exp(exponent: U256) -> Result { let exponent_byte_size = (exponent .bits() diff --git a/crates/vm/levm/src/precompiles.rs b/crates/vm/levm/src/precompiles.rs index d7148d355..1c9412d09 100644 --- a/crates/vm/levm/src/precompiles.rs +++ b/crates/vm/levm/src/precompiles.rs @@ -1,6 +1,15 @@ use bytes::Bytes; use ethrex_core::{Address, H160, U256}; use keccak_hash::keccak256; +use lambdaworks_math::{ + cyclic_group::IsGroup, + elliptic_curve::{ + short_weierstrass::curves::bn_254::curve::{BN254Curve, BN254FieldElement}, + traits::IsEllipticCurve, + }, + traits::ByteConversion, + unsigned_integer::element, +}; use libsecp256k1::{self, Message, RecoveryId, Signature}; use num_bigint::BigUint; use sha3::Digest; @@ -8,7 +17,7 @@ use sha3::Digest; use crate::{ call_frame::CallFrame, errors::{InternalError, OutOfGasError, PrecompileError, VMError}, - gas_cost::{self, ECRECOVER_COST, MODEXP_STATIC_COST}, + gas_cost::{self, ECADD_COST, ECMUL_COST, ECRECOVER_COST, MODEXP_STATIC_COST}, }; pub const ECRECOVER_ADDRESS: H160 = H160([ @@ -356,12 +365,140 @@ pub fn increase_left_pad(result: &Bytes, m_size: usize) -> Result Result { - Ok(Bytes::new()) +pub fn ecadd( + calldata: &Bytes, + gas_for_call: u64, + consumed_gas: &mut u64, +) -> Result { + // 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)?; + + let first_point_y = calldata + .get(32..64) + .ok_or(PrecompileError::ParsingInputError)?; + + let second_point_x = calldata + .get(64..96) + .ok_or(PrecompileError::ParsingInputError)?; + + let second_point_y = calldata + .get(96..128) + .ok_or(PrecompileError::ParsingInputError)?; + + // If points are zero the precompile should not fail, but the conversion in + // BN254Curve::create_point_from_affine will, so we verify it before the conversion + let first_point_is_zero = U256::from_big_endian(first_point_x).is_zero() + && U256::from_big_endian(first_point_y).is_zero(); + let second_point_is_zero = U256::from_big_endian(second_point_x).is_zero() + && U256::from_big_endian(second_point_y).is_zero(); + + let first_point_x = BN254FieldElement::from_bytes_be(first_point_x) + .map_err(|_| PrecompileError::ParsingInputError)?; + let first_point_y = BN254FieldElement::from_bytes_be(first_point_y) + .map_err(|_| PrecompileError::ParsingInputError)?; + let second_point_x = BN254FieldElement::from_bytes_be(second_point_x) + .map_err(|_| PrecompileError::ParsingInputError)?; + let second_point_y = BN254FieldElement::from_bytes_be(second_point_y) + .map_err(|_| PrecompileError::ParsingInputError)?; + + if first_point_is_zero && second_point_is_zero { + // If both points are zero, return is zero + Ok(Bytes::from([0u8; 64].to_vec())) + } else if first_point_is_zero { + // If first point is zero, return is second point + let second_point = BN254Curve::create_point_from_affine(second_point_x, second_point_y) + .map_err(|_| PrecompileError::ParsingInputError)?; + let res = [ + second_point.x().to_bytes_be(), + second_point.y().to_bytes_be(), + ] + .concat(); + Ok(Bytes::from(res)) + } else if second_point_is_zero { + // If second point is zero, return is first point + let first_point = BN254Curve::create_point_from_affine(first_point_x, first_point_y) + .map_err(|_| PrecompileError::ParsingInputError)?; + let res = [first_point.x().to_bytes_be(), first_point.y().to_bytes_be()].concat(); + Ok(Bytes::from(res)) + } else { + // If none of the points is zero, return is the sum of both in the EC + let first_point = BN254Curve::create_point_from_affine(first_point_x, first_point_y) + .map_err(|_| PrecompileError::ParsingInputError)?; + let second_point = BN254Curve::create_point_from_affine(second_point_x, second_point_y) + .map_err(|_| PrecompileError::ParsingInputError)?; + let sum = first_point.operate_with(&second_point).to_affine(); + + if U256::from_big_endian(&sum.x().to_bytes_be()) == U256::zero() + || U256::from_big_endian(&sum.y().to_bytes_be()) == U256::zero() + { + Ok(Bytes::from([0u8; 64].to_vec())) + } else { + let res = [sum.x().to_bytes_be(), sum.y().to_bytes_be()].concat(); + Ok(Bytes::from(res)) + } + } } -fn ecmul(_calldata: &Bytes, _gas_for_call: u64, _consumed_gas: &mut u64) -> Result { - Ok(Bytes::new()) +pub fn ecmul( + calldata: &Bytes, + gas_for_call: u64, + consumed_gas: &mut u64, +) -> Result { + // If calldata does not reach the required length, we should fill the rest with zeros + let calldata = fill_with_zeros(calldata, 96)?; + + increase_precompile_consumed_gas(gas_for_call, ECMUL_COST, consumed_gas)?; + + let point_x = calldata + .get(0..32) + .ok_or(PrecompileError::ParsingInputError)?; + + let point_y = calldata + .get(32..64) + .ok_or(PrecompileError::ParsingInputError)?; + + let scalar = calldata + .get(64..96) + .ok_or(PrecompileError::ParsingInputError)?; + let scalar = + element::U256::from_bytes_be(scalar).map_err(|_| PrecompileError::ParsingInputError)?; + + // If point is zero the precompile should not fail, but the conversion in + // BN254Curve::create_point_from_affine will, so we verify it before the conversion + let point_is_zero = + U256::from_big_endian(point_x).is_zero() && U256::from_big_endian(point_y).is_zero(); + if point_is_zero { + return Ok(Bytes::from([0u8; 64].to_vec())); + } + + let point_x = BN254FieldElement::from_bytes_be(point_x) + .map_err(|_| PrecompileError::ParsingInputError)?; + let point_y = BN254FieldElement::from_bytes_be(point_y) + .map_err(|_| PrecompileError::ParsingInputError)?; + + let point = BN254Curve::create_point_from_affine(point_x, point_y) + .map_err(|_| PrecompileError::ParsingInputError)?; + + let zero_u256 = element::U256::from(0_u16); + if scalar.eq(&zero_u256) { + Ok(Bytes::from([0u8; 64].to_vec())) + } else { + let mul = point.operate_with_self(scalar).to_affine(); + if U256::from_big_endian(&mul.x().to_bytes_be()) == U256::zero() + || U256::from_big_endian(&mul.y().to_bytes_be()) == U256::zero() + { + Ok(Bytes::from([0u8; 64].to_vec())) + } else { + let res = [mul.x().to_bytes_be(), mul.y().to_bytes_be()].concat(); + Ok(Bytes::from(res)) + } + } } fn ecpairing( diff --git a/crates/vm/levm/tests/tests.rs b/crates/vm/levm/tests/tests.rs index fe4b6fa5d..a4b1993fa 100644 --- a/crates/vm/levm/tests/tests.rs +++ b/crates/vm/levm/tests/tests.rs @@ -9,13 +9,13 @@ use ethrex_levm::{ db::{cache, CacheDB, Db}, errors::{OutOfGasError, TxResult, VMError}, gas_cost::{ - self, ECRECOVER_COST, IDENTITY_DYNAMIC_BASE, IDENTITY_STATIC_COST, MODEXP_STATIC_COST, - RIPEMD_160_DYNAMIC_BASE, RIPEMD_160_STATIC_COST, SHA2_256_DYNAMIC_BASE, + 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, }, memory, operations::Operation, - precompiles::{ecrecover, identity, modexp, ripemd_160, sha2_256}, + precompiles::{ecadd, ecmul, 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, @@ -4580,3 +4580,45 @@ fn modexp_test_2() { assert_eq!(result, expected_result); assert_eq!(consumed_gas, MODEXP_STATIC_COST); } +#[test] +fn ecadd_test() { + let calldata = hex::decode("0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002").unwrap(); + let calldata = Bytes::from(calldata); + + let mut consumed_gas = 0; + let result = ecadd(&calldata, 10000, &mut consumed_gas).unwrap(); + + let expected_result = Bytes::from(hex::decode("030644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd315ed738c0e0a7c92e7845f96b2ae9c0a68a6a449e3538fc7ff3ebf7a5a18a2c4").unwrap()); + + assert_eq!(result, expected_result); + assert_eq!(consumed_gas, ECADD_COST); +} + +#[test] +fn ecmul_test() { + let calldata = hex::decode("000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002").unwrap(); + let calldata = Bytes::from(calldata); + + let mut consumed_gas = 0; + let result = ecmul(&calldata, 10000, &mut consumed_gas).unwrap(); + + let expected_result = Bytes::from(hex::decode("030644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd315ed738c0e0a7c92e7845f96b2ae9c0a68a6a449e3538fc7ff3ebf7a5a18a2c4").unwrap()); + + assert_eq!(result, expected_result); + assert_eq!(consumed_gas, ECMUL_COST); +} + +#[test] +fn ecmul_test_2() { + // This tests that an infinite in one coordinate implies a zero result + let calldata = hex::decode("0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000230644e72e131a029b85045b68181585d2833e84879b9709143e1f593f00000010000000000000000000000000000000000000000000000000000000000000000").unwrap(); + let calldata = Bytes::from(calldata); + + let mut consumed_gas = 0; + let result = ecmul(&calldata, 10000, &mut consumed_gas).unwrap(); + + let expected_result = Bytes::from(hex::decode("00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000").unwrap()); + + assert_eq!(result, expected_result); + assert_eq!(consumed_gas, ECMUL_COST); +}