Skip to content

Commit

Permalink
feat(precompile): Prague - EIP-2537 - BLS12-381 curve operations (blu…
Browse files Browse the repository at this point in the history
…ealloy#1389)

* feat(precompile): add Prague hardfork specification

* feat(precompile): add Prague hardfork specification

* feat(precompile): BLS12-381

* feature-gate blst

* run EIP-2537 tests in CI

* fix doc comment

* fixes after review (arrays to vecs, question mark operators, default inits)

* introduce separate variables for mutable blst calls

* return value instead of mutating the input argument where possible

* replace *mut with *const where mut is not needed
  • Loading branch information
shekhirin authored May 10, 2024
1 parent 15ef150 commit 1914696
Show file tree
Hide file tree
Showing 20 changed files with 885 additions and 6 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/ethereum-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,5 @@ jobs:
ethtests/LegacyTests/Constantinople/GeneralStateTests/ \
ethtests/EIPTests/StateTests/stEIP1153-transientStorage/ \
ethtests/EIPTests/StateTests/stEIP4844-blobtransactions/ \
ethtests/EIPTests/StateTests/stEIP2537/ \
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 bins/revme/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ revm = { path = "../../crates/revm", version = "8.0.0", default-features = false
"std",
"serde-json",
"c-kzg",
"blst"
] }
alloy-rlp = { version = "0.3", default-features = false, features = [
"arrayvec",
Expand Down
8 changes: 7 additions & 1 deletion crates/precompile/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,15 @@ secp256k1 = { version = "0.29.0", default-features = false, features = [
"global-context",
], optional = true }

# BLS12-381 precompiles
blst = { version = "0.3.11", optional = true }

[dev-dependencies]
criterion = { version = "0.5" }
rand = { version = "0.8", features = ["std"] }

[features]
default = ["std", "c-kzg", "secp256k1", "portable"]
default = ["std", "c-kzg", "secp256k1", "portable", "blst"]
std = [
"revm-primitives/std",
"k256/std",
Expand Down Expand Up @@ -80,6 +83,9 @@ portable = ["revm-primitives/portable", "c-kzg?/portable"]
# In Linux it passes. If you don't require to build wasm on win/mac, it is safe to use it and it is enabled by default.
secp256k1 = ["dep:secp256k1"]

# Enables the BLS12-381 precompiles.
blst = ["dep:blst"]

[[bench]]
name = "bench"
path = "benches/bench.rs"
Expand Down
48 changes: 48 additions & 0 deletions crates/precompile/src/bls12_381/g1.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
use blst::{blst_fp_from_bendian, blst_p1_affine, blst_p1_affine_in_g1};
use revm_primitives::{Bytes, PrecompileError};

use super::utils::{fp_to_bytes, remove_padding, PADDED_FP_LENGTH};

/// Length of each of the elements in a g1 operation input.
pub(super) 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 a byte slice with padded elements.
pub(super) fn encode_g1_point(input: *const blst_p1_affine) -> Bytes {
let mut out = vec![0u8; G1_OUTPUT_LENGTH];
// SAFETY: out comes 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.into()
}

/// Extracts a G1 point in Affine format from a 128 byte slice representation.
pub(super) fn extract_g1_input(input: &[u8]) -> Result<*const blst_p1_affine, PrecompileError> {
if input.len() != G1_INPUT_ITEM_LENGTH {
return Err(PrecompileError::Other(format!(
"Input should be {G1_INPUT_ITEM_LENGTH} bits, was {}",
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 mut out = blst_p1_affine::default();
// SAFETY: input_p0_x and input_p0_y have fixed length, out is a blst value.
unsafe {
blst_fp_from_bendian(&mut out.x, input_p0_x.as_ptr());
blst_fp_from_bendian(&mut out.y, input_p0_y.as_ptr());
}

// SAFETY: out is a blst value.
unsafe {
if !blst_p1_affine_in_g1(&out) {
return Err(PrecompileError::Other("Element not in G1".to_string()));
}
}
Ok(&mut out as *const _)
}
61 changes: 61 additions & 0 deletions crates/precompile/src/bls12_381/g1_add.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
use blst::{
blst_p1, blst_p1_add_or_double_affine, blst_p1_affine, blst_p1_from_affine, blst_p1_to_affine,
};
use revm_primitives::{Bytes, Precompile, PrecompileError, PrecompileResult};

use crate::{u64_to_address, PrecompileWithAddress};

use super::g1::{encode_g1_point, extract_g1_input, G1_INPUT_ITEM_LENGTH};

/// [EIP-2537](https://eips.ethereum.org/EIPS/eip-2537#specification) BLS12_G1ADD precompile.
pub const PRECOMPILE: PrecompileWithAddress =
PrecompileWithAddress(u64_to_address(ADDRESS), Precompile::Standard(g1_add));
/// BLS12_G1ADD precompile address.
pub const ADDRESS: u64 = 0x0b;
/// Base gas fee for BLS12-381 g1_add operation.
const BASE_GAS_FEE: u64 = 500;

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

/// G1 addition call expects `256` bytes as an input that is interpreted as byte
/// concatenation of two G1 points (`128` bytes each).
/// Output is an encoding of addition operation result - single G1 point (`128`
/// bytes).
/// See also: <https://eips.ethereum.org/EIPS/eip-2537#abi-for-g1-addition>
fn g1_add(input: &Bytes, gas_limit: u64) -> PrecompileResult {
if BASE_GAS_FEE > gas_limit {
return Err(PrecompileError::OutOfGas);
}

if input.len() != INPUT_LENGTH {
return Err(PrecompileError::Other(format!(
"G1ADD Input should be {INPUT_LENGTH} bits, was {}",
input.len()
)));
}

let a_aff = extract_g1_input(&input[..G1_INPUT_ITEM_LENGTH])?;
let b_aff = extract_g1_input(&input[G1_INPUT_ITEM_LENGTH..])?;

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 out = encode_g1_point(&p_aff);
Ok((BASE_GAS_FEE, out))
}
77 changes: 77 additions & 0 deletions crates/precompile/src/bls12_381/g1_msm.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
use blst::{blst_p1, blst_p1_affine, blst_p1_from_affine, blst_p1_to_affine, p1_affines};
use revm_primitives::{Bytes, Precompile, PrecompileError, PrecompileResult};

use crate::{u64_to_address, PrecompileWithAddress};

use super::{
g1::{encode_g1_point, extract_g1_input, G1_INPUT_ITEM_LENGTH},
g1_mul,
msm::msm_required_gas,
utils::{extract_scalar_input, NBITS, SCALAR_LENGTH},
};

/// [EIP-2537](https://eips.ethereum.org/EIPS/eip-2537#specification) BLS12_G1MSM precompile.
pub const PRECOMPILE: PrecompileWithAddress =
PrecompileWithAddress(u64_to_address(ADDRESS), Precompile::Standard(g1_msm));
/// BLS12_G1MSM precompile address.
pub const ADDRESS: u64 = 0x0d;

/// Implements EIP-2537 G1MSM precompile.
/// G1 multi-scalar-multiplication call expects `160*k` bytes as an input that is interpreted
/// as byte concatenation of `k` slices each of them being a byte concatenation
/// of encoding of G1 point (`128` bytes) and encoding of a scalar value (`32`
/// bytes).
/// Output is an encoding of multi-scalar-multiplication operation result - single G1
/// point (`128` bytes).
/// See also: <https://eips.ethereum.org/EIPS/eip-2537#abi-for-g1-multiexponentiation>
fn g1_msm(input: &Bytes, gas_limit: u64) -> PrecompileResult {
let input_len = input.len();
if input_len == 0 || input_len % g1_mul::INPUT_LENGTH != 0 {
return Err(PrecompileError::Other(format!(
"G1MSM input length should be multiple of {}, was {}",
g1_mul::INPUT_LENGTH,
input_len
)));
}

let k = input_len / g1_mul::INPUT_LENGTH;
let required_gas = msm_required_gas(k, g1_mul::BASE_GAS_FEE);
if required_gas > gas_limit {
return Err(PrecompileError::OutOfGas);
}

let mut g1_points: Vec<blst_p1> = Vec::with_capacity(k);
let mut scalars: Vec<u8> = Vec::with_capacity(k * SCALAR_LENGTH);
for i in 0..k {
let p0_aff = extract_g1_input(
&input[i * g1_mul::INPUT_LENGTH..i * g1_mul::INPUT_LENGTH + G1_INPUT_ITEM_LENGTH],
)?;
let mut p0 = blst_p1::default();
// SAFETY: p0 and p0_aff are blst values.
unsafe {
blst_p1_from_affine(&mut p0, p0_aff);
}

g1_points.push(p0);

scalars.extend_from_slice(
&extract_scalar_input(
&input[i * g1_mul::INPUT_LENGTH + G1_INPUT_ITEM_LENGTH
..i * g1_mul::INPUT_LENGTH + G1_INPUT_ITEM_LENGTH + SCALAR_LENGTH],
)?
.b,
);
}

let points = p1_affines::from(&g1_points);
let multiexp = points.mult(&scalars, NBITS);

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

let out = encode_g1_point(&multiexp_aff);
Ok((required_gas, out))
}
61 changes: 61 additions & 0 deletions crates/precompile/src/bls12_381/g1_mul.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
use blst::{blst_p1, blst_p1_affine, blst_p1_from_affine, blst_p1_mult, blst_p1_to_affine};
use revm_primitives::{Bytes, Precompile, PrecompileError, PrecompileResult};

use crate::{u64_to_address, PrecompileWithAddress};

use super::{
g1::{encode_g1_point, extract_g1_input, G1_INPUT_ITEM_LENGTH},
utils::{extract_scalar_input, NBITS},
};

/// [EIP-2537](https://eips.ethereum.org/EIPS/eip-2537#specification) BLS12_G1MUL precompile.
pub const PRECOMPILE: PrecompileWithAddress =
PrecompileWithAddress(u64_to_address(ADDRESS), Precompile::Standard(g1_mul));
/// BLS12_G1MUL precompile address.
pub const ADDRESS: u64 = 0x0c;
/// Base gas fee for BLS12-381 g1_mul operation.
pub(super) const BASE_GAS_FEE: u64 = 12000;

/// Input length of g1_mul operation.
pub(super) const INPUT_LENGTH: usize = 160;

/// G1 multiplication call expects `160` bytes as an input that is interpreted as
/// byte concatenation of encoding of G1 point (`128` bytes) and encoding of a
/// scalar value (`32` bytes).
/// Output is an encoding of multiplication operation result - single G1 point
/// (`128` bytes).
/// See also: <https://eips.ethereum.org/EIPS/eip-2537#abi-for-g1-multiplication>
pub fn g1_mul(input: &Bytes, gas_limit: u64) -> PrecompileResult {
if BASE_GAS_FEE > gas_limit {
return Err(PrecompileError::OutOfGas);
}
if input.len() != INPUT_LENGTH {
return Err(PrecompileError::Other(format!(
"G1MUL Input should be {INPUT_LENGTH} bits, was {}",
input.len()
)));
}

let p0_aff = extract_g1_input(&input[..G1_INPUT_ITEM_LENGTH])?;
let mut p0 = blst_p1::default();
// SAFETY: p0 and p0_aff are blst values.
unsafe {
blst_p1_from_affine(&mut p0, p0_aff);
}

let input_scalar0 = extract_scalar_input(&input[G1_INPUT_ITEM_LENGTH..])?;

let mut p = blst_p1::default();
// SAFETY: input_scalar0.b has fixed size, p and p0 are blst values.
unsafe {
blst_p1_mult(&mut p, &p0, input_scalar0.b.as_ptr(), NBITS);
}
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 out = encode_g1_point(&p_aff);
Ok((BASE_GAS_FEE, out))
}
64 changes: 64 additions & 0 deletions crates/precompile/src/bls12_381/g2.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
use blst::{blst_fp_from_bendian, blst_p2_affine, blst_p2_affine_in_g2};
use revm_primitives::{Bytes, PrecompileError};

use super::utils::{fp_to_bytes, remove_padding, FP_LENGTH, PADDED_FP_LENGTH};

/// Length of each of the elements in a g2 operation input.
pub(super) 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 a byte slice with padded elements.
pub(super) fn encode_g2_point(input: *const blst_p2_affine) -> Bytes {
let mut out = vec![0u8; G2_OUTPUT_LENGTH];
// SAFETY: out comes from fixed length array, input is a blst value.
unsafe {
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.into()
}

/// Extracts a G2 point in Affine format from a 256 byte slice representation.
pub(super) fn extract_g2_input(input: &[u8]) -> Result<*const blst_p2_affine, PrecompileError> {
if input.len() != G2_INPUT_ITEM_LENGTH {
return Err(PrecompileError::Other(format!(
"Input should be {G2_INPUT_ITEM_LENGTH} bits, was {}",
input.len()
)));
}

let mut input_fps: [[u8; FP_LENGTH]; 4] = [[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 mut out = blst_p2_affine::default();
// SAFETY: items in fps have fixed length, out is a blst value.
unsafe {
blst_fp_from_bendian(&mut out.x.fp[0], input_fps[0].as_ptr());
blst_fp_from_bendian(&mut out.x.fp[1], input_fps[1].as_ptr());
blst_fp_from_bendian(&mut out.y.fp[0], input_fps[2].as_ptr());
blst_fp_from_bendian(&mut out.y.fp[1], input_fps[3].as_ptr());
}

// SAFETY: out is a blst value.
unsafe {
if !blst_p2_affine_in_g2(&out) {
return Err(PrecompileError::Other("Element not in G2".to_string()));
}
}

Ok(&mut out as *const _)
}
Loading

0 comments on commit 1914696

Please sign in to comment.