Skip to content

Commit

Permalink
Merge pull request #1416 from oasisprotocol/peternose/feature/bn128
Browse files Browse the repository at this point in the history
runtime-sdk/evm/precomile: Add Ethereum alt-bn128 precompiles
  • Loading branch information
peternose authored Jul 27, 2023
2 parents dafc660 + 5b3b604 commit 9104b4b
Show file tree
Hide file tree
Showing 14 changed files with 1,343 additions and 334 deletions.
296 changes: 156 additions & 140 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions runtime-sdk/modules/evm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ blake3 = { version = "~1.3.1", features = ["traits-preview"] }
thiserror = "1.0"
hex = "0.4.2"
sha2 = "0.9.5"
substrate-bn = "0.6.0"
ripemd160 = { version = "0.9", default-features = false }
k256 = { version = "0.10.4", default-features = false, features = ["keccak256", "ecdsa"] }
sha3 = { version = "0.10", default-features = false }
Expand All @@ -39,6 +40,8 @@ uint = "0.9.1"
criterion = "0.5.1"
oasis-runtime-sdk = { path = "../..", features = ["test"] }
rand = "0.7.3"
serde = { version = "1.0.144", features = ["derive"] }
serde_json = { version = "1.0.87", features = ["raw_value"] }

[[bench]]
name = "criterion_benchmark"
Expand Down
2 changes: 2 additions & 0 deletions runtime-sdk/modules/evm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
#![feature(array_chunks)]
#![feature(test)]

extern crate substrate_bn as bn;

pub mod backend;
pub mod derive_caller;
pub mod precompile;
Expand Down
17 changes: 15 additions & 2 deletions runtime-sdk/modules/evm/src/precompile/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! EVM precompiles.

use std::marker::PhantomData;
use std::{cmp::min, marker::PhantomData};

use evm::{
executor::stack::{PrecompileFailure, PrecompileHandle, PrecompileOutput, PrecompileSet},
Expand Down Expand Up @@ -60,6 +60,16 @@ fn record_multilinear_cost(
Ok(())
}

/// Copies bytes from source to target.
fn read_input(source: &[u8], target: &mut [u8], offset: usize) {
if source.len() <= offset {
return;
}

let len = min(target.len(), source.len() - offset);
target[..len].copy_from_slice(&source[offset..offset + len]);
}

pub(crate) struct Precompiles<'a, Cfg: Config, B: EVMBackendExt> {
backend: &'a B,
config: PhantomData<Cfg>,
Expand Down Expand Up @@ -87,6 +97,9 @@ impl<Cfg: Config, B: EVMBackendExt> PrecompileSet for Precompiles<'_, Cfg, B> {
(0, 0, 3) => standard::call_ripemd160(handle),
(0, 0, 4) => standard::call_datacopy(handle),
(0, 0, 5) => standard::call_bigmodexp(handle),
(0, 0, 6) => standard::call_bn128_add(handle),
(0, 0, 7) => standard::call_bn128_mul(handle),
(0, 0, 8) => standard::call_bn128_pairing(handle),
// Confidential precompiles.
(1, 0, 1) => confidential::call_random_bytes(handle, self.backend),
(1, 0, 2) => confidential::call_x25519_derive(handle),
Expand All @@ -109,7 +122,7 @@ impl<Cfg: Config, B: EVMBackendExt> PrecompileSet for Precompiles<'_, Cfg, B> {
&& matches!(
(a0, a18, a19, Cfg::CONFIDENTIAL),
// Standard.
(0, 0, 1..=5, _) |
(0, 0, 1..=8, _) |
// Confidential.
(1, 0, 1..=8, true) |
// Other.
Expand Down
294 changes: 294 additions & 0 deletions runtime-sdk/modules/evm/src/precompile/standard/bn128.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,294 @@
use bn::{pairing_batch, AffineG1, AffineG2, Fq, Fq2, Fr, Group, Gt, G1, G2};
use evm::{
executor::stack::{PrecompileFailure, PrecompileHandle, PrecompileOutput},
ExitError, ExitSucceed,
};

use crate::precompile::{read_input, PrecompileResult};

/// The gas cost for point addition on the alt-bn128 elliptic curve.
///
/// See https://eips.ethereum.org/EIPS/eip-1108#specification.
const BN128_ADD_GAS_COST: u64 = 150;

/// The gas cost for point multiplication on the alt-bn128 elliptic curve.
///
/// See https://eips.ethereum.org/EIPS/eip-1108#specification.
const BN128_MUL_GAS_COST: u64 = 6000;

/// The base gas cost for pairing on the alt-bn128 elliptic curve.
///
/// See https://eips.ethereum.org/EIPS/eip-1108#specification.
const BN128_PAIRING_BASE_GAS_COST: u64 = 45_000;

/// The check gas cost per pairing on the alt-bn128 elliptic curve.
///
/// See https://eips.ethereum.org/EIPS/eip-1108#specification.
const BN128_PAIRING_CHECK_GAS_COST: u64 = 34_000;

/// Point addition on the alt-bn128 elliptic curve.
pub fn call_bn128_add(handle: &mut impl PrecompileHandle) -> PrecompileResult {
handle.record_cost(BN128_ADD_GAS_COST)?;

let input = handle.input();
let p1 = read_curve_point_g1(input, 0)?;
let p2 = read_curve_point_g1(input, 64)?;
let sum = p1 + p2;
let result = encode_curve_point_g1(sum);

Ok(PrecompileOutput {
exit_status: ExitSucceed::Returned,
output: result.to_vec(),
})
}

/// Point multiplication on the alt-bn128 elliptic curve.
pub fn call_bn128_mul(handle: &mut impl PrecompileHandle) -> PrecompileResult {
handle.record_cost(BN128_MUL_GAS_COST)?;

let input = handle.input();
let p = read_curve_point_g1(input, 0)?;
let s = read_field_element_fr(input, 64)?;
let mul = p * s;
let result = encode_curve_point_g1(mul);

Ok(PrecompileOutput {
exit_status: ExitSucceed::Returned,
output: result.to_vec(),
})
}

/// Pairing check on the alt-bn128 elliptic curve.
pub fn call_bn128_pairing(handle: &mut impl PrecompileHandle) -> PrecompileResult {
let length = handle.input().len();
if length % 192 > 0 {
return Err(PrecompileFailure::Error {
exit_status: ExitError::Other("bad elliptic curve pairing size".into()),
});
}

let num_pairings = length / 192;
handle.record_cost(
BN128_PAIRING_BASE_GAS_COST + BN128_PAIRING_CHECK_GAS_COST * num_pairings as u64,
)?;

let input = handle.input();
let mut pairs = Vec::with_capacity(num_pairings);

for i in 0..num_pairings {
let a = read_curve_point_g1(input, i * 192)?;
let b = read_twist_point_g2(input, i * 192 + 64)?;
pairs.push((a, b));
}
let mul = pairing_batch(&pairs);

let mut result = [0u8; 32];
if mul == Gt::one() {
result[31] = 1;
}

Ok(PrecompileOutput {
exit_status: ExitSucceed::Returned,
output: result.to_vec(),
})
}

/// Decodes elliptic curve point G1.
fn read_curve_point_g1(source: &[u8], offset: usize) -> Result<G1, PrecompileFailure> {
let x = read_field_element_fq(source, offset)?;
let y = read_field_element_fq(source, offset + 32)?;

if x.is_zero() && y.is_zero() {
return Ok(G1::zero());
}

Ok(AffineG1::new(x, y)
.map_err(|_| PrecompileFailure::Error {
exit_status: ExitError::Other("invalid point G1".into()),
})?
.into())
}

/// Decodes elliptic curve point G2.
fn read_twist_point_g2(source: &[u8], offset: usize) -> Result<G2, PrecompileFailure> {
let ay = read_field_element_fq(source, offset)?;
let ax = read_field_element_fq(source, offset + 32)?;
let by = read_field_element_fq(source, offset + 64)?;
let bx = read_field_element_fq(source, offset + 96)?;

let x = Fq2::new(ax, ay);
let y = Fq2::new(bx, by);

if x.is_zero() && y.is_zero() {
return Ok(G2::zero());
}

Ok(AffineG2::new(x, y)
.map_err(|_| PrecompileFailure::Error {
exit_status: ExitError::Other("invalid point G2".into()),
})?
.into())
}

/// Encodes elliptic curve point G1.
fn encode_curve_point_g1(p: G1) -> [u8; 64] {
let mut result = [0u8; 64];
match AffineG1::from_jacobian(p) {
None => (), // Point at infinity.
Some(p) => {
p.x()
.to_big_endian(&mut result[0..32])
.expect("slice has 32-byte length");
p.y()
.to_big_endian(&mut result[32..64])
.expect("slice has 32-byte length");
}
}
result
}

/// Decodes field element Fq.
fn read_field_element_fq(source: &[u8], offset: usize) -> Result<Fq, PrecompileFailure> {
let mut a = [0u8; 32];
read_input(source, &mut a, offset);

Fq::from_slice(&a).map_err(|_| PrecompileFailure::Error {
exit_status: ExitError::Other("invalid field element Fq".into()),
})
}

/// Decodes field element Fr.
fn read_field_element_fr(source: &[u8], offset: usize) -> Result<Fr, PrecompileFailure> {
let mut a = [0u8; 32];
read_input(source, &mut a, offset);

Fr::from_slice(&a).map_err(|_| PrecompileFailure::Error {
exit_status: ExitError::Other("invalid field element Fr".into()),
})
}

#[cfg(test)]
mod test {
use std::fs;

use crate::precompile::testing::*;

use super::*;

/// Test case for precompiled contract tests.
#[derive(serde::Deserialize)]
struct TestCase {
#[serde(rename = "Input")]
pub input: String,

#[serde(rename = "Expected")]
pub expected: String,

#[serde(rename = "Name")]
pub _name: String,

#[serde(default)]
#[serde(rename = "Gas")]
pub gas: u64,

#[serde(default)]
#[serde(rename = "NoBenchmark")]
pub _no_benchmark: bool,
}

/// Reads test cases from the specified file.
///
/// The test cases are from "go-ethereum/core/vm/testdata/precompiles"
/// and from "frontier/frame/evm/precompile/testdata".
///
/// See https://github.com/ethereum/go-ethereum/tree/master/core/vm/testdata/precompiles and
/// https://github.com/paritytech/frontier/tree/master/frame/evm/precompile/testdata.
fn read_test_cases(name: &str) -> Vec<TestCase> {
let path = format!("src/precompile/testdata/{}.json", name);
let contents = fs::read_to_string(path).expect("json file should be readable");

serde_json::from_str(&contents).expect("json decoding should succeed")
}

#[test]
fn test_bn128_add() {
let address = H160([
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x06,
]);

for case in read_test_cases("bn256Add").iter() {
let ret = call_contract(
address,
&hex::decode(case.input.as_str()).unwrap(),
case.gas,
)
.unwrap();
assert_eq!(hex::encode(ret.unwrap().output), case.expected);
}

for case in read_test_cases("common_bnadd").iter() {
let ret = call_contract(
address,
&hex::decode(case.input.as_str()).unwrap(),
BN128_ADD_GAS_COST,
)
.unwrap();
assert_eq!(hex::encode(ret.unwrap().output), case.expected);
}
}

#[test]
fn test_bn128_mul() {
let address = H160([
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x07,
]);

for case in read_test_cases("bn256ScalarMul").iter() {
let ret = call_contract(
address,
&hex::decode(case.input.as_str()).unwrap(),
case.gas,
)
.unwrap();
assert_eq!(hex::encode(ret.unwrap().output), case.expected);
}

for case in read_test_cases("common_bnmul").iter() {
let ret = call_contract(
address,
&hex::decode(case.input.as_str()).unwrap(),
BN128_MUL_GAS_COST,
)
.unwrap();
assert_eq!(hex::encode(ret.unwrap().output), case.expected);
}
}

#[test]
fn test_bn128_pairing() {
let address = H160([
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x08,
]);

for case in read_test_cases("bn256Pairing").iter() {
let ret = call_contract(
address,
&hex::decode(case.input.as_str()).unwrap(),
case.gas,
)
.unwrap();
assert_eq!(hex::encode(ret.unwrap().output), case.expected);
}

for case in read_test_cases("common_bnpair").iter() {
let ret = call_contract(
address,
&hex::decode(case.input.as_str()).unwrap(),
BN128_PAIRING_BASE_GAS_COST
+ BN128_PAIRING_CHECK_GAS_COST * (case.input.len() as u64 / 384),
)
.unwrap();
assert_eq!(hex::encode(ret.unwrap().output), case.expected);
}
}
}
10 changes: 10 additions & 0 deletions runtime-sdk/modules/evm/src/precompile/standard/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//! Implements the standard precompiles as defined in the EVM specification.

mod bn128;
mod modexp;
mod simple;

// Re-exports.
pub(super) use bn128::*;
pub(super) use modexp::*;
pub(super) use simple::*;
Loading

0 comments on commit 9104b4b

Please sign in to comment.