Skip to content
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

Feat: EIP-2537 - Precompiles for BLS12-381 curve operations #990

Open
wants to merge 12 commits into
base: feat/prague-hard-fork
Choose a base branch
from
Next Next commit
Added basic BLS12-381 functions & G1_ADD precompile
mrLSD committed Jan 30, 2025

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
commit 092308b365a828893d45b161654afac55872a4d3
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions engine-precompiles/Cargo.toml
Original file line number Diff line number Diff line change
@@ -23,6 +23,7 @@ num.workspace = true
ripemd.workspace = true
sha2.workspace = true
sha3.workspace = true
blst = "0.3.13"

[dev-dependencies]
aurora-engine-test-doubles.workspace = true
85 changes: 85 additions & 0 deletions engine-precompiles/src/bls12_381/g1.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
use super::{fp_from_bendian, fp_to_bytes, remove_padding, PADDED_FP_LENGTH};
use crate::prelude::{vec, Borrowed, Vec};
use blst::{blst_p1_affine, blst_p1_affine_in_g1, blst_p1_affine_on_curve};
use evm::ExitError;

/// Length of each of the elements in a g1 operation input.
pub const G1_INPUT_ITEM_LENGTH: usize = 128;

/// Output length of a g1 operation.
const G1_OUTPUT_LENGTH: usize = 128;

/// Encodes a G1 point in affine format into byte slice with padded elements.
pub fn encode_g1_point(input: *const blst_p1_affine) -> Vec<u8> {
let mut out = vec![0u8; G1_OUTPUT_LENGTH];
// SAFETY: outcomes from fixed length array, input is a blst value.
unsafe {
fp_to_bytes(&mut out[..PADDED_FP_LENGTH], &(*input).x);
fp_to_bytes(&mut out[PADDED_FP_LENGTH..], &(*input).y);
}
out
}

/// Returns a `blst_p1_affine` from the provided byte slices, which represent the x and y
/// affine coordinates of the point.
///
/// If the x or y coordinate do not represent a canonical field element, an error is returned.
///
/// See [`fp_from_bendian`] for more information.
pub fn decode_and_check_g1(p0_x: &[u8; 48], p0_y: &[u8; 48]) -> Result<blst_p1_affine, ExitError> {
let out = blst_p1_affine {
x: fp_from_bendian(p0_x)?,
y: fp_from_bendian(p0_y)?,
};

Ok(out)
}

/// Extracts a G1 point in Affine format from a 128 byte slice representation.
///
/// NOTE: This function will perform a G1 subgroup check if `subgroup_check` is set to `true`.
pub fn extract_g1_input(input: &[u8], subgroup_check: bool) -> Result<blst_p1_affine, ExitError> {
if input.len() != G1_INPUT_ITEM_LENGTH {
return Err(ExitError::Other(Borrowed("ERR_BLS12_G1_INPUT_LEN")));
}

let input_p0_x = remove_padding(&input[..PADDED_FP_LENGTH])?;
let input_p0_y = remove_padding(&input[PADDED_FP_LENGTH..G1_INPUT_ITEM_LENGTH])?;
let out = decode_and_check_g1(input_p0_x, input_p0_y)?;

if subgroup_check {
// NB: Subgroup checks
//
// Scalar multiplications, MSMs and pairings MUST perform a subgroup check.
//
// Implementations SHOULD use the optimized subgroup check method:
//
// https://eips.ethereum.org/assets/eip-2537/fast_subgroup_checks
//
// On any input that fail the subgroup check, the precompile MUST return an error.
//
// As endomorphism acceleration requires input on the correct subgroup, implementers MAY
// use endomorphism acceleration.
if unsafe { !blst_p1_affine_in_g1(&out) } {
return Err(ExitError::Other(Borrowed("ERR_BLS12_ELEMENT_NOT_IN_G1")));
}
} else {
// From EIP-2537:
//
// Error cases:
//
// * An input is neither a point on the G1 elliptic curve nor the infinity point
//
// NB: There is no subgroup check for the G1 addition precompile.
//
// We use blst_p1_affine_on_curve instead of blst_p1_affine_in_g1 because the latter performs
// the subgroup check.
//
// SAFETY: out is a blst value.
if unsafe { !blst_p1_affine_on_curve(&out) } {
return Err(ExitError::Other(Borrowed("ERR_BLS12_ELEMENT_NOT_IN_G1")));
}
}

Ok(out)
}
70 changes: 70 additions & 0 deletions engine-precompiles/src/bls12_381/g1_add.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
use super::g1;
use crate::prelude::types::{make_address, Address, EthGas};
use crate::prelude::Borrowed;
use crate::{EvmPrecompileResult, Precompile, PrecompileOutput};
use blst::{
blst_p1, blst_p1_add_or_double_affine, blst_p1_affine, blst_p1_from_affine, blst_p1_to_affine,
};
use evm::{Context, ExitError};

/// Base gas fee for BLS12-381 `g1_add` operation.
const BASE_GAS_FEE: u64 = 375;

/// Input length of `g1_add` operation.
const INPUT_LENGTH: usize = 256;

/// BLS12-382 G1 Add
pub struct BlsG1Add;

impl BlsG1Add {
pub const ADDRESS: Address = make_address(0, 0xB);
}

impl Precompile for BlsG1Add {
fn required_gas(_input: &[u8]) -> Result<EthGas, ExitError>
where
Self: Sized,
{
Ok(EthGas::new(BASE_GAS_FEE))
}

fn run(
&self,
input: &[u8],
target_gas: Option<EthGas>,
_context: &Context,
_is_static: bool,
) -> EvmPrecompileResult {
let cost = Self::required_gas(input)?;
if let Some(target_gas) = target_gas {
if cost > target_gas {
return Err(ExitError::OutOfGas);
}
}

if input.len() != INPUT_LENGTH {
return Err(ExitError::Other(Borrowed("ERR_BLS_G1ADD_INPUT_LEN")));
}

// NB: There is no subgroup check for the G1 addition precompile.
//
// We set the subgroup checks here to `false`
let a_aff = &g1::extract_g1_input(&input[..g1::G1_INPUT_ITEM_LENGTH], false)?;
let b_aff = &g1::extract_g1_input(&input[g1::G1_INPUT_ITEM_LENGTH..], false)?;

let mut b = blst_p1::default();
// SAFETY: b and b_aff are blst values.
unsafe { blst_p1_from_affine(&mut b, b_aff) };

let mut p = blst_p1::default();
// SAFETY: p, b and a_aff are blst values.
unsafe { blst_p1_add_or_double_affine(&mut p, &b, a_aff) };

let mut p_aff = blst_p1_affine::default();
// SAFETY: p_aff and p are blst values.
unsafe { blst_p1_to_affine(&mut p_aff, &p) };

let output = g1::encode_g1_point(&p_aff);
Ok(PrecompileOutput::without_logs(cost, output))
}
}
Empty file.
106 changes: 106 additions & 0 deletions engine-precompiles/src/bls12_381/g2.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
#![allow(dead_code)]

use super::{fp_from_bendian, fp_to_bytes, remove_padding, FP_LENGTH, PADDED_FP_LENGTH};
use crate::prelude::{vec, Borrowed, Vec};
use blst::{blst_fp2, blst_p2_affine, blst_p2_affine_in_g2, blst_p2_affine_on_curve};
use evm::ExitError;

/// Length of each of the elements in a g2 operation input.
pub const G2_INPUT_ITEM_LENGTH: usize = 256;

/// Output length of a g2 operation.
const G2_OUTPUT_LENGTH: usize = 256;

/// Encodes a G2 point in affine format into byte slice with padded elements.
pub fn encode_g2_point(input: &blst_p2_affine) -> Vec<u8> {
let mut out = vec![0u8; G2_OUTPUT_LENGTH];
fp_to_bytes(&mut out[..PADDED_FP_LENGTH], &input.x.fp[0]);
fp_to_bytes(
&mut out[PADDED_FP_LENGTH..2 * PADDED_FP_LENGTH],
&input.x.fp[1],
);
fp_to_bytes(
&mut out[2 * PADDED_FP_LENGTH..3 * PADDED_FP_LENGTH],
&input.y.fp[0],
);
fp_to_bytes(
&mut out[3 * PADDED_FP_LENGTH..4 * PADDED_FP_LENGTH],
&input.y.fp[1],
);
out
}

/// Convert the following field elements from byte slices into a `blst_p2_affine` point.
pub fn decode_and_check_g2(
x1: &[u8; 48],
x2: &[u8; 48],
y1: &[u8; 48],
y2: &[u8; 48],
) -> Result<blst_p2_affine, ExitError> {
Ok(blst_p2_affine {
x: check_canonical_fp2(x1, x2)?,
y: check_canonical_fp2(y1, y2)?,
})
}

/// Checks whether or not the input represents a canonical fp2 field element, returning the field
/// element if successful.
pub fn check_canonical_fp2(input_1: &[u8; 48], input_2: &[u8; 48]) -> Result<blst_fp2, ExitError> {
let fp_1 = fp_from_bendian(input_1)?;
let fp_2 = fp_from_bendian(input_2)?;

let fp2 = blst_fp2 { fp: [fp_1, fp_2] };
Ok(fp2)
}

/// Extracts a G2 point in Affine format from a 256 byte slice representation.
///
/// NOTE: This function will perform a G2 subgroup check if `subgroup_check` is set to `true`.
pub fn extract_g2_input(input: &[u8], subgroup_check: bool) -> Result<blst_p2_affine, ExitError> {
if input.len() != G2_INPUT_ITEM_LENGTH {
return Err(ExitError::Other(Borrowed("ERR_BLS12_G2_INPUT_LEN")));
}

let mut input_fps = [&[0; FP_LENGTH]; 4];
for i in 0..4 {
input_fps[i] = remove_padding(&input[i * PADDED_FP_LENGTH..(i + 1) * PADDED_FP_LENGTH])?;
}

let out = decode_and_check_g2(input_fps[0], input_fps[1], input_fps[2], input_fps[3])?;

if subgroup_check {
// NB: Subgroup checks
//
// Scalar multiplications, MSMs and pairings MUST perform a subgroup check.
//
// Implementations SHOULD use the optimized subgroup check method:
//
// https://eips.ethereum.org/assets/eip-2537/fast_subgroup_checks
//
// On any input that fail the subgroup check, the precompile MUST return an error.
//
// As endomorphism acceleration requires input on the correct subgroup, implementers MAY
// use endomorphism acceleration.
if unsafe { !blst_p2_affine_in_g2(&out) } {
return Err(ExitError::Other(Borrowed("ERR_BLS12_ELEMENT_NOT_IN_G2")));
}
} else {
// From EIP-2537:
//
// Error cases:
//
// * An input is neither a point on the G2 elliptic curve nor the infinity point
//
// NB: There is no subgroup check for the G2 addition precompile.
//
// We use blst_p2_affine_on_curve instead of blst_p2_affine_in_g2 because the latter performs
// the subgroup check.
//
// SAFETY: out is a blst value.
if unsafe { !blst_p2_affine_on_curve(&out) } {
return Err(ExitError::Other(Borrowed("ERR_BLS12_ELEMENT_NOT_IN_G2")));
}
}

Ok(out)
}
Empty file.
Empty file.
Empty file.
1 change: 1 addition & 0 deletions engine-precompiles/src/bls12_381/map_fp_to_g1.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

143 changes: 143 additions & 0 deletions engine-precompiles/src/bls12_381/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
#![allow(dead_code)]

//! # BLS12-382
//!
//! Represents [EIP-2537](https://eips.ethereum.org/EIPS/eip-2537)
use crate::prelude::Borrowed;
use crate::utils;
use blst::{
blst_bendian_from_fp, blst_fp, blst_fp_from_bendian, blst_scalar, blst_scalar_from_bendian,
};
use evm::ExitError;

mod g1;
mod g1_add;
mod g2;
// mod g1_msm;
// mod g2_add;
// mod g2_msm;
// mod map_fp_to_g1;
// mod pairing_check;

pub use g1_add::BlsG1Add;

/// Number of bits used in the BLS12-381 curve finite field elements.
const NBITS: usize = 256;
/// Finite field element input length.
const FP_LENGTH: usize = 48;
/// Finite field element padded input length.
const PADDED_FP_LENGTH: usize = 64;
/// Quadratic extension of finite field element input length.
const PADDED_FP2_LENGTH: usize = 128;
/// Input elements padding length.
const PADDING_LENGTH: usize = 16;
/// Scalar length.
const SCALAR_LENGTH: usize = 32;
// Big-endian non-Montgomery form.
const MODULUS_REPR: [u8; 48] = [
0x1a, 0x01, 0x11, 0xea, 0x39, 0x7f, 0xe6, 0x9a, 0x4b, 0x1b, 0xa7, 0xb6, 0x43, 0x4b, 0xac, 0xd7,
0x64, 0x77, 0x4b, 0x84, 0xf3, 0x85, 0x12, 0xbf, 0x67, 0x30, 0xd2, 0xa0, 0xf6, 0xb0, 0xf6, 0x24,
0x1e, 0xab, 0xff, 0xfe, 0xb1, 0x53, 0xff, 0xff, 0xb9, 0xfe, 0xff, 0xff, 0xff, 0xff, 0xaa, 0xab,
];
/// Amount used to calculate the multi-scalar-multiplication discount.
const MSM_MULTIPLIER: u64 = 1000;

/// BLS Encodes a single finite field element into byte slice with padding.
fn fp_to_bytes(out: &mut [u8], input: *const blst_fp) {
if out.len() != PADDED_FP_LENGTH {
return;
}
let (padding, rest) = out.split_at_mut(PADDING_LENGTH);
padding.fill(0);
unsafe { blst_bendian_from_fp(rest.as_mut_ptr(), input) };
}

/// Checks if the input is a valid big-endian representation of a field element.
fn is_valid_be(input: &[u8; 48]) -> bool {
for (i, modul) in input.iter().zip(MODULUS_REPR.iter()) {
match i.cmp(modul) {
core::cmp::Ordering::Greater => return false,
core::cmp::Ordering::Less => return true,
core::cmp::Ordering::Equal => continue,
}
}
// false if matching the modulus
false
}

/// Checks whether or not the input represents a canonical field element, returning the field
/// element if successful.
fn fp_from_bendian(input: &[u8; 48]) -> Result<blst_fp, ExitError> {
if !is_valid_be(input) {
return Err(ExitError::Other(Borrowed("ERR_BLS12_INVALID_FP_VALUE")));
}
let mut fp = blst_fp::default();
// SAFETY: input has fixed length, and fp is a blst value.
unsafe {
// This performs the check for canonical field elements
blst_fp_from_bendian(&mut fp, input.as_ptr());
}
Ok(fp)
}

/// Removes zeros with which the precompile inputs are left padded to 64 bytes.
fn remove_padding(input: &[u8]) -> Result<&[u8; FP_LENGTH], ExitError> {
if input.len() != PADDED_FP_LENGTH {
return Err(ExitError::Other(Borrowed("ERR_BLS12_PADDED_FP_LENGTH")));
}
let (padding, unpadded) = input.split_at(PADDING_LENGTH);
if !padding.iter().all(|&x| x == 0) {
return Err(ExitError::Other(Borrowed(
"ERR_BLS12_PADDED_FP_LENGTH_NOT_ZERO",
)));
}
unpadded
.try_into()
.map_err(|_| ExitError::Other(Borrowed("ERR_BLS12_FAIL_PADDING")))
}

/// Extracts a scalar from a 32 byte slice representation, decoding the input as a big endian
/// unsigned integer. If the input is not exactly 32 bytes long, an error is returned.
///
/// From [EIP-2537](https://eips.ethereum.org/EIPS/eip-2537):
/// * A scalar for the multiplication operation is encoded as 32 bytes by performing `BigEndian`
/// encoding of the corresponding (unsigned) integer.
///
/// We do not check that the scalar is a canonical Fr element, because the EIP specifies:
/// * The corresponding integer is not required to be less than or equal than main subgroup order
/// `q`.
fn extract_scalar_input(input: &[u8]) -> Result<blst_scalar, ExitError> {
if input.len() != SCALAR_LENGTH {
return Err(ExitError::Other(Borrowed("ERR_BLS12_SCALAR_INPUT")));
}

let mut out = blst_scalar::default();
// SAFETY: input length is checked previously, out is a blst value.
unsafe {
// NOTE: we do not use `blst_scalar_fr_check` here because, from EIP-2537:
//
// * The corresponding integer is not required to be less than or equal than main subgroup
// order `q`.
blst_scalar_from_bendian(&mut out, input.as_ptr());
};

Ok(out)
}

/// Implements the gas schedule for G1/G2 Multiscalar-multiplication assuming 30
/// MGas/second, see also: <https://eips.ethereum.org/EIPS/eip-2537#g1g2-multiexponentiation>
fn msm_required_gas(
k: usize,
discount_table: &[u16],
multiplication_cost: u64,
) -> Result<u64, ExitError> {
if k == 0 {
return Ok(0);
}

let index = core::cmp::min(k - 1, discount_table.len() - 1);
let discount = u64::from(discount_table[index]);

let k = u64::try_from(k).map_err(utils::err_usize_conv)?;
Ok((k * discount * multiplication_cost) / MSM_MULTIPLIER)
}
Empty file.
45 changes: 44 additions & 1 deletion engine-precompiles/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
#![cfg_attr(not(feature = "std"), no_std)]
#![forbid(unsafe_code)]

pub mod account_ids;
pub mod alt_bn256;
pub mod blake2;
pub mod bls12_381;
pub mod hash;
pub mod identity;
pub mod modexp;
@@ -344,6 +344,49 @@ impl<'a, I: IO + Copy, E: Env, H: ReadOnlyPromiseHandler> Precompiles<'a, I, E,
Self::new_berlin(ctx)
}

/// Prague hard fork includes `BLS12-381` precompiles.
///
/// [EIP-2537](https://eips.ethereum.org/EIPS/eip-2537)
pub fn new_prague<M: ModExpAlgorithm + 'static>(
ctx: PrecompileConstructorContext<'a, I, E, H, M>,
) -> Self {
let addresses = vec![
ECRecover::ADDRESS,
SHA256::ADDRESS,
RIPEMD160::ADDRESS,
Identity::ADDRESS,
ModExp::<Berlin, M>::ADDRESS,
Bn256Add::<Istanbul>::ADDRESS,
Bn256Mul::<Istanbul>::ADDRESS,
Bn256Pair::<Istanbul>::ADDRESS,
Blake2F::ADDRESS,
RandomSeed::ADDRESS,
CurrentAccount::ADDRESS,
bls12_381::BlsG1Add::ADDRESS,
];
let fun: Vec<Box<dyn Precompile>> = vec![
Box::new(ECRecover),
Box::new(SHA256),
Box::new(RIPEMD160),
Box::new(Identity),
Box::new(ModExp::<Berlin, M>::new()),
Box::new(Bn256Add::<Istanbul>::new()),
Box::new(Bn256Mul::<Istanbul>::new()),
Box::new(Bn256Pair::<Istanbul>::new()),
Box::new(Blake2F),
Box::new(RandomSeed::new(ctx.random_seed)),
Box::new(CurrentAccount::new(ctx.current_account_id.clone())),
Box::new(bls12_381::BlsG1Add),
];
let map = addresses
.into_iter()
.zip(fun)
.map(|(a, f)| (a, AllPrecompiles::Generic(f)))
.collect();

Self::with_generic_precompiles(map, ctx)
}

fn with_generic_precompiles<M: ModExpAlgorithm + 'static>(
mut generic_precompiles: BTreeMap<Address, AllPrecompiles<'a, I, E, H>>,
ctx: PrecompileConstructorContext<'a, I, E, H, M>,
2 changes: 1 addition & 1 deletion engine/src/engine.rs
Original file line number Diff line number Diff line change
@@ -906,7 +906,7 @@ impl<'env, I: IO + Copy, E: Env, M: ModExpAlgorithm> Engine<'env, I, E, M> {
let env = self.env;
let ro_promise_handler = handler.read_only();

let precompiles = Precompiles::new_london(PrecompileConstructorContext {
let precompiles = Precompiles::new_prague(PrecompileConstructorContext {
current_account_id,
random_seed,
io,