diff --git a/.github/workflows/tests-accelerators.yml b/.github/workflows/tests-accelerators.yml new file mode 100644 index 000000000..e4907dafa --- /dev/null +++ b/.github/workflows/tests-accelerators.yml @@ -0,0 +1,67 @@ +name: OpenVM Accelerators Tests + +on: + pull_request: + paths: + - "crates/accelerators/**" + - "Cargo.toml" + - "Cargo.lock" + - ".github/workflows/tests-accelerators.yml" + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true + +env: + CARGO_TERM_COLOR: always + CARGO_NET_GIT_FETCH_WITH_CLI: "true" + +jobs: + test: + runs-on: + - runs-on=${{ github.run_id }} + - runner=64cpu-linux-arm64 + - extras=s3-cache + + steps: + - uses: runs-on/action@v2 + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@nightly + - uses: Swatinem/rust-cache@v2 + with: + cache-on-failure: true + - uses: taiki-e/install-action@nextest + + - name: Run tests + run: cargo nextest run -p openvm-accelerators + + guest-build: + runs-on: + - runs-on=${{ github.run_id }} + - runner=64cpu-linux-arm64 + - extras=s3-cache + + steps: + - uses: runs-on/action@v2 + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: "1.91.1" + - uses: Swatinem/rust-cache@v2 + with: + cache-on-failure: true + - name: Cache openvm toolchain + uses: actions/cache@v4 + with: + path: ~/.openvm/toolchains + key: openvm-toolchain-openvm-1.94.0-${{ runner.os }}-${{ runner.arch }} + + - name: Install OpenVM CLI + run: | + cargo install --git https://github.com/openvm-org/openvm.git --branch develop-v2.1.0 --locked --force cargo-openvm + cargo openvm toolchain install + + - name: Build for the guest target + run: | + cd crates/accelerators + cargo openvm build --no-transpile diff --git a/.github/workflows/tests-revm-crypto.yml b/.github/workflows/tests-revm-crypto.yml index 0333bd062..bdc30d44d 100644 --- a/.github/workflows/tests-revm-crypto.yml +++ b/.github/workflows/tests-revm-crypto.yml @@ -4,6 +4,7 @@ on: pull_request: paths: - "crates/revm-crypto/**" + - "crates/accelerators/**" - "crates/kzg/**" - "crates/curve-utils/**" - "Cargo.toml" diff --git a/Cargo.lock b/Cargo.lock index d07466a53..05abc19af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6785,6 +6785,28 @@ dependencies = [ "serde", ] +[[package]] +name = "openvm-accelerators" +version = "0.4.0" +dependencies = [ + "ark-bls12-381", + "ark-ec", + "ark-serialize 0.5.0", + "aurora-engine-modexp", + "hex-literal", + "k256 0.13.4 (registry+https://github.com/rust-lang/crates.io-index)", + "k256 0.13.4 (git+https://github.com/openvm-org/openvm.git?branch=develop-v2.1.0)", + "openvm-curve-utils", + "openvm-ecc-guest", + "openvm-keccak256", + "openvm-kzg", + "openvm-pairing", + "openvm-pairing-guest", + "openvm-sha2", + "p256 0.13.2 (git+https://github.com/openvm-org/openvm.git?branch=develop-v2.1.0)", + "ripemd", +] + [[package]] name = "openvm-algebra-circuit" version = "2.0.0" @@ -7729,15 +7751,7 @@ version = "0.4.0" dependencies = [ "alloy-consensus", "alloy-primitives", - "aurora-engine-modexp", - "k256 0.13.4 (git+https://github.com/openvm-org/openvm.git?branch=develop-v2.1.0)", - "openvm-curve-utils", - "openvm-ecc-guest", - "openvm-keccak256", - "openvm-kzg", - "openvm-pairing", - "openvm-sha2", - "p256 0.13.2 (git+https://github.com/openvm-org/openvm.git?branch=develop-v2.1.0)", + "openvm-accelerators", "revm 40.0.3", "revm-primitives 24.0.1", ] diff --git a/Cargo.toml b/Cargo.toml index 19035c747..7606f8b2f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ members = [ "crates/curve-utils", "crates/kzg", "crates/kzg/tests/programs/verify_kzg", + "crates/accelerators", ] exclude = [] resolver = "3" @@ -58,6 +59,7 @@ zstd = { version = "0.13", default-features = false } # workspace openvm-curve-utils = { path = "./crates/curve-utils", default-features = false } +openvm-accelerators = { path = "./crates/accelerators", default-features = false } openvm-kzg = { path = "./crates/kzg", default-features = false } openvm-mpt = { path = "./crates/mpt" } openvm-revm-crypto = { path = "./crates/revm-crypto" } @@ -111,6 +113,7 @@ openvm-circuit = { git = "https://github.com/openvm-org/openvm.git", branch = "d openvm-verify-stark-host = { git = "https://github.com/openvm-org/openvm.git", branch = "develop-v2.1.0", default-features = false } openvm-ecc-guest = { git = "https://github.com/openvm-org/openvm.git", branch = "develop-v2.1.0", default-features = false } openvm-keccak256 = { git = "https://github.com/openvm-org/openvm.git", branch = "develop-v2.1.0", default-features = false } +openvm-pairing-guest = { git = "https://github.com/openvm-org/openvm.git", branch = "develop-v2.1.0", default-features = false } openvm-pairing = { git = "https://github.com/openvm-org/openvm.git", branch = "develop-v2.1.0", default-features = false, features = [ "bn254", "bls12_381", diff --git a/bin/stateless-guest/Cargo.lock b/bin/stateless-guest/Cargo.lock index 46c3bb510..330271d54 100644 --- a/bin/stateless-guest/Cargo.lock +++ b/bin/stateless-guest/Cargo.lock @@ -2463,6 +2463,27 @@ dependencies = [ "serde", ] +[[package]] +name = "openvm-accelerators" +version = "0.4.0" +dependencies = [ + "ark-bls12-381", + "ark-ec", + "ark-serialize 0.5.0", + "aurora-engine-modexp", + "k256 0.13.4 (registry+https://github.com/rust-lang/crates.io-index)", + "k256 0.13.4 (git+https://github.com/openvm-org/openvm.git?branch=develop-v2.1.0)", + "openvm-curve-utils", + "openvm-ecc-guest", + "openvm-keccak256", + "openvm-kzg", + "openvm-pairing", + "openvm-pairing-guest", + "openvm-sha2", + "p256 0.13.2 (git+https://github.com/openvm-org/openvm.git?branch=develop-v2.1.0)", + "ripemd", +] + [[package]] name = "openvm-algebra-complex-macros" version = "2.0.0" @@ -2627,6 +2648,7 @@ version = "2.0.0" source = "git+https://github.com/openvm-org/openvm.git?branch=develop-v2.1.0#ec5180e9ee15234aff8070febcbb2733a2adad74" dependencies = [ "group", + "halo2curves-axiom", "hex-literal", "itertools 0.14.0", "num-bigint", @@ -2649,6 +2671,7 @@ name = "openvm-pairing-guest" version = "2.0.0" source = "git+https://github.com/openvm-org/openvm.git?branch=develop-v2.1.0#ec5180e9ee15234aff8070febcbb2733a2adad74" dependencies = [ + "halo2curves-axiom", "hex-literal", "itertools 0.14.0", "lazy_static", @@ -2681,15 +2704,7 @@ version = "0.4.0" dependencies = [ "alloy-consensus", "alloy-primitives", - "aurora-engine-modexp", - "k256 0.13.4 (git+https://github.com/openvm-org/openvm.git?branch=develop-v2.1.0)", - "openvm-curve-utils", - "openvm-ecc-guest", - "openvm-keccak256", - "openvm-kzg", - "openvm-pairing", - "openvm-sha2", - "p256 0.13.2 (git+https://github.com/openvm-org/openvm.git?branch=develop-v2.1.0)", + "openvm-accelerators", "revm", "revm-primitives", ] diff --git a/crates/accelerators/Cargo.toml b/crates/accelerators/Cargo.toml new file mode 100644 index 000000000..8479efdc2 --- /dev/null +++ b/crates/accelerators/Cargo.toml @@ -0,0 +1,56 @@ +[package] +name = "openvm-accelerators" +description = "OpenVM cryptographic accelerator interface" +version.workspace = true +edition.workspace = true +homepage.workspace = true +repository.workspace = true + +[lints] +workspace = true + +[dependencies] +# openvm +openvm-curve-utils = { workspace = true, features = ["bn254", "bls12_381"] } +openvm-ecc-guest.workspace = true +openvm-kzg = { workspace = true, features = ["use-intrinsics"] } +openvm-p256.workspace = true +openvm-pairing = { workspace = true, features = ["bn254", "bls12_381"] } +openvm-keccak256.workspace = true +openvm-sha2.workspace = true + +# crypto +ark-bls12-381 = { version = "0.5", default-features = false, features = ["curve"] } +ark-ec = { version = "0.5", default-features = false } +ark-serialize = { version = "0.5", default-features = false } +aurora-engine-modexp = { version = "1.2.0", default-features = false } +ripemd = { version = "0.1.3", default-features = false } + +# The OpenVM-accelerated k256 fork; its ECDSA recovery relies on zkVM hints +# and is unimplemented outside the guest. +[target.'cfg(any(target_os = "none", target_os = "openvm"))'.dependencies] +openvm-k256.workspace = true + +# Host implementations when not building for the zkVM guest. +[target.'cfg(not(any(target_os = "none", target_os = "openvm")))'.dependencies] +k256 = { version = "0.13", default-features = false, features = ["ecdsa"] } +openvm-keccak256 = { workspace = true, features = ["tiny_keccak"] } +# Not imported directly: enables the host pairing backend used by the KZG +# verification. openvm-pairing's `halo2curves` feature does not forward to +# openvm-pairing-guest, so the inner feature is enabled explicitly. +openvm-pairing = { workspace = true, features = ["halo2curves"] } +openvm-pairing-guest = { workspace = true, features = ["halo2curves"] } +openvm-sha2 = { workspace = true, features = ["import_sha2"] } + +[dev-dependencies] +hex-literal.workspace = true + +[package.metadata.cargo-shear] +ignored = ["openvm-pairing-guest"] + +[package.metadata.cargo-machete] +ignored = ["openvm-pairing-guest"] + +[features] +default = [] +std = [] diff --git a/crates/accelerators/src/blake2f.rs b/crates/accelerators/src/blake2f.rs new file mode 100644 index 000000000..76689a19a --- /dev/null +++ b/crates/accelerators/src/blake2f.rs @@ -0,0 +1,166 @@ +//! BLAKE2b compression function F (EIP-152). +//! +//! Operates on raw BLAKE2b state with an arbitrary round count. + +// Ported from revm-precompile 36.0.3's EIP-152 adaptation: +// https://docs.rs/crate/revm-precompile/36.0.3/source/src/blake2/portable.rs +// That implementation is adapted from blake2b_simd: +// https://github.com/oconnor663/blake2_simd +// +// Copyright (c) 2018 Jack O'Connor +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +use crate::types::{zkvm_status, ZkvmBytes, ZKVM_EFAIL, ZKVM_EOK}; + +pub type zkvm_blake2f_state = ZkvmBytes<64>; +pub type zkvm_blake2f_message = ZkvmBytes<128>; +pub type zkvm_blake2f_offset = ZkvmBytes<16>; + +/// Apply BLAKE2 compression function F to `h` in place. +/// +/// # Safety +/// +/// - `h` must be valid for reads and writes of one [`zkvm_blake2f_state`]. +/// - `m` must be valid for reads of one [`zkvm_blake2f_message`]. +/// - `t` must be valid for reads of one [`zkvm_blake2f_offset`]. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn zkvm_blake2f( + rounds: u32, + h: *mut zkvm_blake2f_state, + m: *const zkvm_blake2f_message, + t: *const zkvm_blake2f_offset, + f: u8, +) -> zkvm_status { + if h.is_null() || m.is_null() || t.is_null() || f > 1 { + return ZKVM_EFAIL; + } + + // SAFETY: the non-null inputs satisfy the function's pointer requirements. + // Read every input before writing `h` so overlapping arguments are supported. + let (state, message, offset) = unsafe { (h.read(), m.read(), t.read()) }; + + let mut state_words = [0; 8]; + for (word, bytes) in state_words.iter_mut().zip(state.data.as_chunks::<8>().0) { + *word = Word::from_le_bytes(*bytes); + } + let mut message_words = [0; 16]; + for (word, bytes) in message_words.iter_mut().zip(message.data.as_chunks::<8>().0) { + *word = Word::from_le_bytes(*bytes); + } + let offset_words = [ + Word::from_le_bytes(offset.data[..8].try_into().unwrap()), + Word::from_le_bytes(offset.data[8..].try_into().unwrap()), + ]; + + compress(rounds, &mut state_words, &message_words, &offset_words, f == 1); + + let mut value = zkvm_blake2f_state { data: [0; 64] }; + for (bytes, word) in value.data.as_chunks_mut::<8>().0.iter_mut().zip(state_words) { + *bytes = word.to_le_bytes(); + } + + // SAFETY: `h` satisfies the function's write requirement; all reads are complete. + unsafe { h.write(value) }; + ZKVM_EOK +} + +type Word = u64; + +const IV: [Word; 8] = [ + 0x6A09E667F3BCC908, + 0xBB67AE8584CAA73B, + 0x3C6EF372FE94F82B, + 0xA54FF53A5F1D36F1, + 0x510E527FADE682D1, + 0x9B05688C2B3E6C1F, + 0x1F83D9ABFB41BD6B, + 0x5BE0CD19137E2179, +]; + +// The message schedule has period 10 (RFC 7693 section 2.7). EIP-152 permits +// arbitrary round counts, so rounds beyond the standard 12 must use `r % 10`. +const SIGMA: [[u8; 16]; 10] = [ + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], + [14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3], + [11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4], + [7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8], + [9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13], + [2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9], + [12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11], + [13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10], + [6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5], + [10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0], +]; + +#[inline(always)] +const fn g(v: &mut [Word; 16], a: usize, b: usize, c: usize, d: usize, x: Word, y: Word) { + v[a] = v[a].wrapping_add(v[b]).wrapping_add(x); + v[d] = (v[d] ^ v[a]).rotate_right(32); + v[c] = v[c].wrapping_add(v[d]); + v[b] = (v[b] ^ v[c]).rotate_right(24); + v[a] = v[a].wrapping_add(v[b]).wrapping_add(y); + v[d] = (v[d] ^ v[a]).rotate_right(16); + v[c] = v[c].wrapping_add(v[d]); + v[b] = (v[b] ^ v[c]).rotate_right(63); +} + +#[inline(always)] +const fn round(round: usize, m: &[Word; 16], v: &mut [Word; 16]) { + let schedule = SIGMA[round % SIGMA.len()]; + + g(v, 0, 4, 8, 12, m[schedule[0] as usize], m[schedule[1] as usize]); + g(v, 1, 5, 9, 13, m[schedule[2] as usize], m[schedule[3] as usize]); + g(v, 2, 6, 10, 14, m[schedule[4] as usize], m[schedule[5] as usize]); + g(v, 3, 7, 11, 15, m[schedule[6] as usize], m[schedule[7] as usize]); + + g(v, 0, 5, 10, 15, m[schedule[8] as usize], m[schedule[9] as usize]); + g(v, 1, 6, 11, 12, m[schedule[10] as usize], m[schedule[11] as usize]); + g(v, 2, 7, 8, 13, m[schedule[12] as usize], m[schedule[13] as usize]); + g(v, 3, 4, 9, 14, m[schedule[14] as usize], m[schedule[15] as usize]); +} + +fn compress(rounds: u32, h: &mut [Word; 8], m: &[Word; 16], t: &[Word; 2], f: bool) { + let mut v = [ + h[0], + h[1], + h[2], + h[3], + h[4], + h[5], + h[6], + h[7], + IV[0], + IV[1], + IV[2], + IV[3], + IV[4] ^ t[0], + IV[5] ^ t[1], + IV[6] ^ if f { Word::MAX } else { 0 }, + IV[7], + ]; + + for round_index in 0..rounds as usize { + round(round_index, m, &mut v); + } + + for (index, word) in h.iter_mut().enumerate() { + *word ^= v[index] ^ v[index + 8]; + } +} diff --git a/crates/accelerators/src/bls12_381/codec.rs b/crates/accelerators/src/bls12_381/codec.rs new file mode 100644 index 000000000..205be15ba --- /dev/null +++ b/crates/accelerators/src/bls12_381/codec.rs @@ -0,0 +1,90 @@ +//! EIP-2537 BLS12-381 point and scalar codecs. + +use openvm_curve_utils::SubgroupCheck; +use openvm_ecc_guest::{algebra::IntMod, weierstrass::WeierstrassPoint, Group}; +use openvm_pairing::bls12_381 as bls; + +use crate::error::Error; + +const FP_LEN: usize = 48; + +#[inline] +fn read_fp(input: &[u8]) -> Result { + bls::Fp::from_be_bytes(input).ok_or(Error::FieldElementInvalid) +} + +#[inline] +fn read_fp2(c0: &[u8], c1: &[u8]) -> Result { + Ok(bls::Fp2::new(read_fp(c0)?, read_fp(c1)?)) +} + +#[inline] +pub(super) fn read_g1_no_subgroup_check(input: &[u8; 96]) -> Result { + let x = read_fp(&input[..FP_LEN])?; + let y = read_fp(&input[FP_LEN..])?; + // SAFETY: the coordinates are canonical; `from_xy` checks the curve equation. + unsafe { bls::G1Affine::from_xy(x, y) }.ok_or(Error::PointNotOnCurve) +} + +#[inline] +pub(super) fn read_g1(input: &[u8; 96]) -> Result { + let point = read_g1_no_subgroup_check(input)?; + point.is_in_correct_subgroup().then_some(point).ok_or(Error::PointNotInSubgroup) +} + +#[inline] +pub(super) fn read_g2_no_subgroup_check(input: &[u8; 192]) -> Result { + let x = read_fp2(&input[..FP_LEN], &input[FP_LEN..2 * FP_LEN])?; + let y = read_fp2(&input[2 * FP_LEN..3 * FP_LEN], &input[3 * FP_LEN..])?; + // SAFETY: the coordinates are canonical; `from_xy` checks the twist equation. + unsafe { bls::G2Affine::from_xy(x, y) }.ok_or(Error::PointNotOnCurve) +} + +#[inline] +pub(super) fn read_g2(input: &[u8; 192]) -> Result { + let point = read_g2_no_subgroup_check(input)?; + point.is_in_correct_subgroup().then_some(point).ok_or(Error::PointNotInSubgroup) +} + +#[inline] +pub(super) fn read_scalar(input: &[u8; 32]) -> bls::Scalar { + bls::Scalar::from_be_bytes_unchecked(input) +} + +#[inline] +pub(super) fn encode_g1(point: &bls::G1Affine) -> [u8; 96] { + let mut output = [0; 96]; + if point.is_identity() { + return output; + } + + let x: &[u8] = point.x().as_le_bytes(); + let y: &[u8] = point.y().as_le_bytes(); + for index in 0..FP_LEN { + output[index] = x[FP_LEN - 1 - index]; + output[index + FP_LEN] = y[FP_LEN - 1 - index]; + } + output +} + +#[inline] +pub(super) fn encode_g2(point: &bls::G2Affine) -> [u8; 192] { + let mut output = [0; 192]; + if point.is_identity() { + return output; + } + + let x = point.x(); + let y = point.y(); + let x_c0 = x.c0.as_le_bytes(); + let x_c1 = x.c1.as_le_bytes(); + let y_c0 = y.c0.as_le_bytes(); + let y_c1 = y.c1.as_le_bytes(); + for index in 0..FP_LEN { + output[index] = x_c0[FP_LEN - 1 - index]; + output[index + FP_LEN] = x_c1[FP_LEN - 1 - index]; + output[index + 2 * FP_LEN] = y_c0[FP_LEN - 1 - index]; + output[index + 3 * FP_LEN] = y_c1[FP_LEN - 1 - index]; + } + output +} diff --git a/crates/accelerators/src/bls12_381/map.rs b/crates/accelerators/src/bls12_381/map.rs new file mode 100644 index 000000000..51cd772e1 --- /dev/null +++ b/crates/accelerators/src/bls12_381/map.rs @@ -0,0 +1,64 @@ +//! BLS12-381 map-to-curve operations. + +use ark_bls12_381::{Fq, Fq2, G1Affine, G2Affine}; +use ark_ec::{ + hashing::{curve_maps::wb::WBMap, map_to_curve_hasher::MapToCurve}, + AffineRepr, +}; +use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; + +use crate::error::Error; + +const FP_LEN: usize = 48; + +#[inline] +pub(super) fn fp_to_g1(input: &[u8; 48]) -> Result<[u8; 96], Error> { + let point = WBMap::map_to_curve(read_fq(input)?) + .expect("the arkworks WB map is defined for every field element") + .clear_cofactor(); + Ok(encode_g1(&point)) +} + +#[inline] +pub(super) fn fp2_to_g2(input: &[u8; 96]) -> Result<[u8; 192], Error> { + let c0 = read_fq(&input[..FP_LEN])?; + let c1 = read_fq(&input[FP_LEN..])?; + let point = WBMap::map_to_curve(Fq2::new(c0, c1)) + .expect("the arkworks WB map is defined for every field element") + .clear_cofactor(); + Ok(encode_g2(&point)) +} + +fn read_fq(input_be: &[u8]) -> Result { + let mut input_le = [0; FP_LEN]; + input_le.copy_from_slice(input_be); + input_le.reverse(); + Fq::deserialize_uncompressed(&input_le[..]).map_err(|_| Error::FieldElementInvalid) +} + +fn encode_fq(fq: &Fq, output: &mut [u8]) { + fq.serialize_uncompressed(&mut output[..]).expect("field element serialization is infallible"); + output.reverse(); +} + +fn encode_g1(point: &G1Affine) -> [u8; 96] { + let mut output = [0; 96]; + let Some((x, y)) = point.xy() else { + return output; + }; + encode_fq(&x, &mut output[..FP_LEN]); + encode_fq(&y, &mut output[FP_LEN..]); + output +} + +fn encode_g2(point: &G2Affine) -> [u8; 192] { + let mut output = [0; 192]; + let Some((x, y)) = point.xy() else { + return output; + }; + encode_fq(&x.c0, &mut output[..FP_LEN]); + encode_fq(&x.c1, &mut output[FP_LEN..2 * FP_LEN]); + encode_fq(&y.c0, &mut output[2 * FP_LEN..3 * FP_LEN]); + encode_fq(&y.c1, &mut output[3 * FP_LEN..]); + output +} diff --git a/crates/accelerators/src/bls12_381/mod.rs b/crates/accelerators/src/bls12_381/mod.rs new file mode 100644 index 000000000..95173f371 --- /dev/null +++ b/crates/accelerators/src/bls12_381/mod.rs @@ -0,0 +1,290 @@ +//! BLS12-381 accelerators (EIP-2537). + +mod codec; +mod map; + +use alloc::vec::Vec; + +use crate::types::{zkvm_status, ZkvmBytes, ZKVM_EFAIL, ZKVM_EOK}; +use codec::{ + encode_g1, encode_g2, read_g1, read_g1_no_subgroup_check, read_g2, read_g2_no_subgroup_check, + read_scalar, +}; +use openvm_ecc_guest::{ + weierstrass::{IntrinsicCurve, WeierstrassPoint}, + AffinePoint, +}; +use openvm_pairing::{bls12_381::Bls12_381, PairingCheck}; + +use crate::error::Error; + +pub type zkvm_bls12_381_g1_point = ZkvmBytes<96>; +pub type zkvm_bls12_381_g2_point = ZkvmBytes<192>; +pub type zkvm_bls12_381_scalar = ZkvmBytes<32>; +pub type zkvm_bls12_381_fp = ZkvmBytes<48>; +pub type zkvm_bls12_381_fp2 = ZkvmBytes<96>; + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct zkvm_bls12_381_g1_msm_pair { + pub point: zkvm_bls12_381_g1_point, + pub scalar: zkvm_bls12_381_scalar, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct zkvm_bls12_381_g2_msm_pair { + pub point: zkvm_bls12_381_g2_point, + pub scalar: zkvm_bls12_381_scalar, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct zkvm_bls12_381_pairing_pair { + pub g1: zkvm_bls12_381_g1_point, + pub g2: zkvm_bls12_381_g2_point, +} + +/// BLS12-381 G1 point addition. +/// +/// # Safety +/// +/// Each pointer must be non-NULL and valid for one value of its pointee type. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn zkvm_bls12_g1_add( + p1: *const zkvm_bls12_381_g1_point, + p2: *const zkvm_bls12_381_g1_point, + result: *mut zkvm_bls12_381_g1_point, +) -> zkvm_status { + if p1.is_null() || p2.is_null() || result.is_null() { + return ZKVM_EFAIL; + } + // SAFETY: the caller guarantees valid reads. The output is written only after these + // shared borrows are no longer used, so overlapping storage is supported. + let value = unsafe { g1_add(&(*p1).data, &(*p2).data) }; + match value { + Ok(data) => { + // SAFETY: `result` is non-NULL and valid for writes. + unsafe { result.write(zkvm_bls12_381_g1_point { data }) }; + ZKVM_EOK + } + Err(_) => ZKVM_EFAIL, + } +} + +/// BLS12-381 G1 multi-scalar multiplication. +/// +/// # Safety +/// +/// `pairs` must be valid for `num_pairs` reads when non-empty, and `result` +/// must be non-NULL and valid for one write. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn zkvm_bls12_g1_msm( + pairs: *const zkvm_bls12_381_g1_msm_pair, + num_pairs: usize, + result: *mut zkvm_bls12_381_g1_point, +) -> zkvm_status { + if result.is_null() || (pairs.is_null() && num_pairs != 0) { + return ZKVM_EFAIL; + } + // SAFETY: the caller guarantees that non-empty input is valid for `num_pairs` reads. + let pairs = + if num_pairs == 0 { &[] } else { unsafe { core::slice::from_raw_parts(pairs, num_pairs) } }; + match g1_msm(pairs) { + Ok(data) => { + // SAFETY: all input reads are complete; `result` is valid for writes. + unsafe { result.write(zkvm_bls12_381_g1_point { data }) }; + ZKVM_EOK + } + Err(_) => ZKVM_EFAIL, + } +} + +/// BLS12-381 G2 point addition. +/// +/// # Safety +/// +/// Each pointer must be non-NULL and valid for one value of its pointee type. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn zkvm_bls12_g2_add( + p1: *const zkvm_bls12_381_g2_point, + p2: *const zkvm_bls12_381_g2_point, + result: *mut zkvm_bls12_381_g2_point, +) -> zkvm_status { + if p1.is_null() || p2.is_null() || result.is_null() { + return ZKVM_EFAIL; + } + // SAFETY: the caller guarantees valid reads. The output is written only after these + // shared borrows are no longer used, so overlapping storage is supported. + let value = unsafe { g2_add(&(*p1).data, &(*p2).data) }; + match value { + Ok(data) => { + // SAFETY: `result` is non-NULL and valid for writes. + unsafe { result.write(zkvm_bls12_381_g2_point { data }) }; + ZKVM_EOK + } + Err(_) => ZKVM_EFAIL, + } +} + +/// BLS12-381 G2 multi-scalar multiplication. +/// +/// # Safety +/// +/// `pairs` must be valid for `num_pairs` reads when non-empty, and `result` +/// must be non-NULL and valid for one write. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn zkvm_bls12_g2_msm( + pairs: *const zkvm_bls12_381_g2_msm_pair, + num_pairs: usize, + result: *mut zkvm_bls12_381_g2_point, +) -> zkvm_status { + if result.is_null() || (pairs.is_null() && num_pairs != 0) { + return ZKVM_EFAIL; + } + // SAFETY: the caller guarantees that non-empty input is valid for `num_pairs` reads. + let pairs = + if num_pairs == 0 { &[] } else { unsafe { core::slice::from_raw_parts(pairs, num_pairs) } }; + match g2_msm(pairs) { + Ok(data) => { + // SAFETY: all input reads are complete; `result` is valid for writes. + unsafe { result.write(zkvm_bls12_381_g2_point { data }) }; + ZKVM_EOK + } + Err(_) => ZKVM_EFAIL, + } +} + +/// BLS12-381 pairing check. +/// +/// # Safety +/// +/// `pairs` must be valid for `num_pairs` reads when non-empty, and `verified` +/// must be non-NULL and valid for one write. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn zkvm_bls12_pairing( + pairs: *const zkvm_bls12_381_pairing_pair, + num_pairs: usize, + verified: *mut bool, +) -> zkvm_status { + if verified.is_null() || (pairs.is_null() && num_pairs != 0) { + return ZKVM_EFAIL; + } + // SAFETY: the caller guarantees that non-empty input is valid for `num_pairs` reads. + let pairs = + if num_pairs == 0 { &[] } else { unsafe { core::slice::from_raw_parts(pairs, num_pairs) } }; + match pairing(pairs) { + Ok(value) => { + // SAFETY: all input reads are complete; `verified` is valid for writes. + unsafe { verified.write(value) }; + ZKVM_EOK + } + Err(_) => ZKVM_EFAIL, + } +} + +/// Map a BLS12-381 base-field element to G1. +/// +/// # Safety +/// +/// Each pointer must be non-NULL and valid for one value of its pointee type. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn zkvm_bls12_map_fp_to_g1( + field_element: *const zkvm_bls12_381_fp, + result: *mut zkvm_bls12_381_g1_point, +) -> zkvm_status { + if field_element.is_null() || result.is_null() { + return ZKVM_EFAIL; + } + // SAFETY: the caller guarantees a valid read. The output is written only after this + // shared borrow is no longer used, so overlapping storage is supported. + let value = unsafe { map::fp_to_g1(&(*field_element).data) }; + match value { + Ok(data) => { + // SAFETY: `result` is non-NULL and valid for writes. + unsafe { result.write(zkvm_bls12_381_g1_point { data }) }; + ZKVM_EOK + } + Err(_) => ZKVM_EFAIL, + } +} + +/// Map a BLS12-381 quadratic-extension-field element to G2. +/// +/// # Safety +/// +/// Each pointer must be non-NULL and valid for one value of its pointee type. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn zkvm_bls12_map_fp2_to_g2( + field_element: *const zkvm_bls12_381_fp2, + result: *mut zkvm_bls12_381_g2_point, +) -> zkvm_status { + if field_element.is_null() || result.is_null() { + return ZKVM_EFAIL; + } + // SAFETY: the caller guarantees a valid read. The output is written only after this + // shared borrow is no longer used, so overlapping storage is supported. + let value = unsafe { map::fp2_to_g2(&(*field_element).data) }; + match value { + Ok(data) => { + // SAFETY: `result` is non-NULL and valid for writes. + unsafe { result.write(zkvm_bls12_381_g2_point { data }) }; + ZKVM_EOK + } + Err(_) => ZKVM_EFAIL, + } +} + +#[inline] +fn g1_add(p1: &[u8; 96], p2: &[u8; 96]) -> Result<[u8; 96], Error> { + Ok(encode_g1(&(read_g1_no_subgroup_check(p1)? + read_g1_no_subgroup_check(p2)?))) +} + +fn g1_msm(pairs: &[zkvm_bls12_381_g1_msm_pair]) -> Result<[u8; 96], Error> { + let mut points = Vec::with_capacity(pairs.len()); + let mut scalars = Vec::with_capacity(pairs.len()); + for pair in pairs { + points.push(read_g1(&pair.point.data)?); + scalars.push(read_scalar(&pair.scalar.data)); + } + if points.is_empty() { + Ok([0; 96]) + } else { + Ok(encode_g1(&Bls12_381::msm(&scalars, &points))) + } +} + +#[inline] +fn g2_add(p1: &[u8; 192], p2: &[u8; 192]) -> Result<[u8; 192], Error> { + Ok(encode_g2(&(read_g2_no_subgroup_check(p1)? + read_g2_no_subgroup_check(p2)?))) +} + +fn g2_msm(pairs: &[zkvm_bls12_381_g2_msm_pair]) -> Result<[u8; 192], Error> { + let mut points = Vec::with_capacity(pairs.len()); + let mut scalars = Vec::with_capacity(pairs.len()); + for pair in pairs { + points.push(read_g2(&pair.point.data)?); + scalars.push(read_scalar(&pair.scalar.data)); + } + if points.is_empty() { + Ok([0; 192]) + } else { + Ok(encode_g2(&openvm_ecc_guest::msm(&scalars, &points))) + } +} + +fn pairing(pairs: &[zkvm_bls12_381_pairing_pair]) -> Result { + let mut g1_points = Vec::with_capacity(pairs.len()); + let mut g2_points = Vec::with_capacity(pairs.len()); + for pair in pairs { + let (g1_x, g1_y) = read_g1(&pair.g1.data)?.into_coords(); + let (g2_x, g2_y) = read_g2(&pair.g2.data)?.into_coords(); + g1_points.push(AffinePoint::new(g1_x, g1_y)); + g2_points.push(AffinePoint::new(g2_x, g2_y)); + } + if g1_points.is_empty() { + Ok(true) + } else { + Ok(Bls12_381::pairing_check(&g1_points, &g2_points).is_ok()) + } +} diff --git a/crates/accelerators/src/bn254/codec.rs b/crates/accelerators/src/bn254/codec.rs new file mode 100644 index 000000000..b766e5712 --- /dev/null +++ b/crates/accelerators/src/bn254/codec.rs @@ -0,0 +1,54 @@ +use openvm_curve_utils::SubgroupCheck; +use openvm_ecc_guest::{algebra::IntMod, weierstrass::WeierstrassPoint}; +use openvm_pairing::bn254 as bn; + +use crate::error::Error; + +const FQ_LEN: usize = 32; + +#[inline] +fn read_fq(input: &[u8]) -> Result { + bn::Fp::from_be_bytes(input).ok_or(Error::FieldElementInvalid) +} + +#[inline] +fn read_fq2(input: &[u8; 64]) -> Result { + let imag = read_fq(&input[..FQ_LEN])?; + let real = read_fq(&input[FQ_LEN..])?; + Ok(bn::Fp2::new(real, imag)) +} + +#[inline] +pub(super) fn read_g1(input: &[u8; 64]) -> Result { + let x = read_fq(&input[..FQ_LEN])?; + let y = read_fq(&input[FQ_LEN..])?; + // SAFETY: the coordinates are canonical; `from_xy` checks the curve equation. + let point = unsafe { bn::G1Affine::from_xy(x, y) }.ok_or(Error::PointNotOnCurve)?; + point.is_in_correct_subgroup().then_some(point).ok_or(Error::PointNotInSubgroup) +} + +#[inline] +pub(super) fn read_g2(input: &[u8; 128]) -> Result { + let x = read_fq2(input[..64].try_into().unwrap())?; + let y = read_fq2(input[64..].try_into().unwrap())?; + // SAFETY: the coordinates are canonical; `from_xy` checks the curve equation. + let point = unsafe { bn::G2Affine::from_xy(x, y) }.ok_or(Error::PointNotOnCurve)?; + point.is_in_correct_subgroup().then_some(point).ok_or(Error::PointNotInSubgroup) +} + +#[inline] +pub(super) fn read_scalar(input: &[u8; 32]) -> bn::Scalar { + bn::Scalar::from_be_bytes_unchecked(input) +} + +#[inline] +pub(super) fn encode_g1(point: bn::G1Affine) -> [u8; 64] { + let mut output = [0; 64]; + let x: &[u8] = point.x().as_le_bytes(); + let y: &[u8] = point.y().as_le_bytes(); + for index in 0..FQ_LEN { + output[index] = x[FQ_LEN - 1 - index]; + output[index + FQ_LEN] = y[FQ_LEN - 1 - index]; + } + output +} diff --git a/crates/accelerators/src/bn254/mod.rs b/crates/accelerators/src/bn254/mod.rs new file mode 100644 index 000000000..e8549f2ce --- /dev/null +++ b/crates/accelerators/src/bn254/mod.rs @@ -0,0 +1,136 @@ +//! BN254 accelerators (EIP-196 and EIP-197). + +mod codec; + +use alloc::vec::Vec; + +use crate::types::{zkvm_status, ZkvmBytes, ZKVM_EFAIL, ZKVM_EOK}; +use codec::{encode_g1, read_g1, read_g2, read_scalar}; +use openvm_ecc_guest::{ + weierstrass::{IntrinsicCurve, WeierstrassPoint}, + AffinePoint, +}; +use openvm_pairing::{bn254::Bn254, PairingCheck}; + +use crate::error::Error; + +pub type zkvm_bn254_g1_point = ZkvmBytes<64>; +pub type zkvm_bn254_g2_point = ZkvmBytes<128>; +pub type zkvm_bn254_scalar = ZkvmBytes<32>; + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct zkvm_bn254_pairing_pair { + pub g1: zkvm_bn254_g1_point, + pub g2: zkvm_bn254_g2_point, +} + +/// Add two BN254 G1 points. +/// +/// # Safety +/// +/// Each pointer must be non-NULL and valid for one value of its pointee type. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn zkvm_bn254_g1_add( + p1: *const zkvm_bn254_g1_point, + p2: *const zkvm_bn254_g1_point, + result: *mut zkvm_bn254_g1_point, +) -> zkvm_status { + if p1.is_null() || p2.is_null() || result.is_null() { + return ZKVM_EFAIL; + } + // SAFETY: the caller guarantees valid reads. The output is written only after these + // shared borrows are no longer used, so overlapping storage is supported. + let value = unsafe { g1_add(&(*p1).data, &(*p2).data) }; + match value { + Ok(data) => { + // SAFETY: `result` is non-NULL and valid for writes. + unsafe { result.write(zkvm_bn254_g1_point { data }) }; + ZKVM_EOK + } + Err(_) => ZKVM_EFAIL, + } +} + +/// Multiply a BN254 G1 point by a scalar. +/// +/// # Safety +/// +/// Each pointer must be non-NULL and valid for one value of its pointee type. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn zkvm_bn254_g1_mul( + point: *const zkvm_bn254_g1_point, + scalar: *const zkvm_bn254_scalar, + result: *mut zkvm_bn254_g1_point, +) -> zkvm_status { + if point.is_null() || scalar.is_null() || result.is_null() { + return ZKVM_EFAIL; + } + // SAFETY: the caller guarantees valid reads. The output is written only after these + // shared borrows are no longer used, so overlapping storage is supported. + let value = unsafe { g1_mul(&(*point).data, &(*scalar).data) }; + match value { + Ok(data) => { + // SAFETY: `result` is non-NULL and valid for writes. + unsafe { result.write(zkvm_bn254_g1_point { data }) }; + ZKVM_EOK + } + Err(_) => ZKVM_EFAIL, + } +} + +/// Check a BN254 pairing equation. +/// +/// # Safety +/// +/// `pairs` must be valid for `num_pairs` reads when non-empty, and `verified` +/// must be non-NULL and valid for one write. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn zkvm_bn254_pairing( + pairs: *const zkvm_bn254_pairing_pair, + num_pairs: usize, + verified: *mut bool, +) -> zkvm_status { + if verified.is_null() || (pairs.is_null() && num_pairs != 0) { + return ZKVM_EFAIL; + } + // SAFETY: the caller guarantees that non-empty input is valid for `num_pairs` reads. + let pairs = + if num_pairs == 0 { &[] } else { unsafe { core::slice::from_raw_parts(pairs, num_pairs) } }; + let value = pairing(pairs.iter().map(|pair| (&pair.g1.data, &pair.g2.data))); + match value { + Ok(value) => { + // SAFETY: `verified` is non-NULL and all input reads are complete. + unsafe { verified.write(value) }; + ZKVM_EOK + } + Err(_) => ZKVM_EFAIL, + } +} + +fn g1_add(p1: &[u8; 64], p2: &[u8; 64]) -> Result<[u8; 64], Error> { + Ok(encode_g1(read_g1(p1)? + read_g1(p2)?)) +} + +fn g1_mul(point: &[u8; 64], scalar: &[u8; 32]) -> Result<[u8; 64], Error> { + Ok(encode_g1(Bn254::msm(&[read_scalar(scalar)], &[read_g1(point)?]))) +} + +fn pairing<'a>( + pairs: impl IntoIterator, +) -> Result { + let pairs = pairs.into_iter(); + let mut g1_points = Vec::with_capacity(pairs.size_hint().0); + let mut g2_points = Vec::with_capacity(pairs.size_hint().0); + for (g1, g2) in pairs { + let (g1_x, g1_y) = read_g1(g1)?.into_coords(); + let (g2_x, g2_y) = read_g2(g2)?.into_coords(); + g1_points.push(AffinePoint::new(g1_x, g1_y)); + g2_points.push(AffinePoint::new(g2_x, g2_y)); + } + if g1_points.is_empty() { + Ok(true) + } else { + Ok(Bn254::pairing_check(&g1_points, &g2_points).is_ok()) + } +} diff --git a/crates/accelerators/src/error.rs b/crates/accelerators/src/error.rs new file mode 100644 index 000000000..3928c2744 --- /dev/null +++ b/crates/accelerators/src/error.rs @@ -0,0 +1,8 @@ +//! Shared curve-decoding errors. + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum Error { + FieldElementInvalid, + PointNotOnCurve, + PointNotInSubgroup, +} diff --git a/crates/accelerators/src/keccak256.rs b/crates/accelerators/src/keccak256.rs new file mode 100644 index 000000000..bc111693c --- /dev/null +++ b/crates/accelerators/src/keccak256.rs @@ -0,0 +1,31 @@ +//! Keccak-256 accelerator. + +use crate::types::{zkvm_status, ZkvmBytes, ZKVM_EFAIL, ZKVM_EOK}; + +pub type zkvm_keccak256_hash = ZkvmBytes<32>; + +/// Compute the Keccak-256 hash of `data[..len]` into `output`. +/// +/// A NULL `data` pointer is accepted only when `len == 0`. +/// +/// # Safety +/// +/// - `data`, if non-NULL, must be valid for reads of `len` bytes. +/// - `output`, if non-NULL, must be valid for writes of one [`zkvm_keccak256_hash`]. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn zkvm_keccak256( + data: *const u8, + len: usize, + output: *mut zkvm_keccak256_hash, +) -> zkvm_status { + if output.is_null() || (data.is_null() && len != 0) { + return ZKVM_EFAIL; + } + // SAFETY: non-NULL checked above; validity is guaranteed by the caller. + let data = if len == 0 { &[] } else { unsafe { core::slice::from_raw_parts(data, len) } }; + let value = zkvm_keccak256_hash { data: openvm_keccak256::keccak256(data) }; + // SAFETY: `output` is non-NULL and valid for writes. All input reads are complete, so + // overlapping input/output storage is supported. + unsafe { output.write(value) }; + ZKVM_EOK +} diff --git a/crates/accelerators/src/kzg.rs b/crates/accelerators/src/kzg.rs new file mode 100644 index 000000000..83982e247 --- /dev/null +++ b/crates/accelerators/src/kzg.rs @@ -0,0 +1,48 @@ +//! KZG point-evaluation accelerator (EIP-4844). + +use crate::types::{zkvm_status, ZkvmBytes, ZKVM_EFAIL, ZKVM_EOK}; +use openvm_kzg::{Bytes32, Bytes48, KzgProof}; + +pub type zkvm_kzg_commitment = ZkvmBytes<48>; +pub type zkvm_kzg_proof = ZkvmBytes<48>; +pub type zkvm_kzg_field_element = ZkvmBytes<32>; + +/// Verify a KZG point-evaluation proof. +/// +/// # Safety +/// +/// Every pointer must be non-NULL and valid for one read or write of its pointee type. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn zkvm_kzg_point_eval( + commitment: *const zkvm_kzg_commitment, + z: *const zkvm_kzg_field_element, + y: *const zkvm_kzg_field_element, + proof: *const zkvm_kzg_proof, + verified: *mut bool, +) -> zkvm_status { + if commitment.is_null() || z.is_null() || y.is_null() || proof.is_null() || verified.is_null() { + return ZKVM_EFAIL; + } + // SAFETY: the caller guarantees valid reads. `verified` is written only after these + // shared borrows are no longer used, so overlapping storage is supported. + let value = unsafe { verify(&(*commitment).data, &(*z).data, &(*y).data, &(*proof).data) } + .unwrap_or(false); + // SAFETY: `verified` is non-NULL and valid for writes. + unsafe { verified.write(value) }; + ZKVM_EOK +} + +fn verify(commitment: &[u8; 48], z: &[u8; 32], y: &[u8; 32], proof: &[u8; 48]) -> Result { + let commitment = Bytes48::from_slice(commitment).map_err(|_| ())?; + let z = Bytes32::from_slice(z).map_err(|_| ())?; + let y = Bytes32::from_slice(y).map_err(|_| ())?; + let proof = Bytes48::from_slice(proof).map_err(|_| ())?; + KzgProof::verify_kzg_proof( + &commitment, + &z, + &y, + &proof, + openvm_kzg::EnvKzgSettings::default().get(), + ) + .map_err(|_| ()) +} diff --git a/crates/accelerators/src/lib.rs b/crates/accelerators/src/lib.rs new file mode 100644 index 000000000..1ae378709 --- /dev/null +++ b/crates/accelerators/src/lib.rs @@ -0,0 +1,44 @@ +//! OpenVM implementation of the standard zkVM accelerator C interface. + +#![cfg_attr(not(feature = "std"), no_std)] +#![allow(non_camel_case_types)] + +extern crate alloc; + +mod blake2f; +mod bls12_381; +mod bn254; +mod error; +mod keccak256; +mod kzg; +mod modexp; +mod ripemd160; +mod secp256k1; +mod secp256r1; +mod sha256; +mod types; + +pub use blake2f::{zkvm_blake2f, zkvm_blake2f_message, zkvm_blake2f_offset, zkvm_blake2f_state}; +pub use bls12_381::{ + zkvm_bls12_381_fp, zkvm_bls12_381_fp2, zkvm_bls12_381_g1_msm_pair, zkvm_bls12_381_g1_point, + zkvm_bls12_381_g2_msm_pair, zkvm_bls12_381_g2_point, zkvm_bls12_381_pairing_pair, + zkvm_bls12_381_scalar, zkvm_bls12_g1_add, zkvm_bls12_g1_msm, zkvm_bls12_g2_add, + zkvm_bls12_g2_msm, zkvm_bls12_map_fp2_to_g2, zkvm_bls12_map_fp_to_g1, zkvm_bls12_pairing, +}; +pub use bn254::{ + zkvm_bn254_g1_add, zkvm_bn254_g1_mul, zkvm_bn254_g1_point, zkvm_bn254_g2_point, + zkvm_bn254_pairing, zkvm_bn254_pairing_pair, zkvm_bn254_scalar, +}; +pub use keccak256::{zkvm_keccak256, zkvm_keccak256_hash}; +pub use kzg::{zkvm_kzg_commitment, zkvm_kzg_field_element, zkvm_kzg_point_eval, zkvm_kzg_proof}; +pub use modexp::zkvm_modexp; +pub use ripemd160::{zkvm_ripemd160, zkvm_ripemd160_hash}; +pub use secp256k1::{ + zkvm_secp256k1_ecrecover, zkvm_secp256k1_hash, zkvm_secp256k1_pubkey, zkvm_secp256k1_signature, + zkvm_secp256k1_verify, +}; +pub use secp256r1::{ + zkvm_secp256r1_hash, zkvm_secp256r1_pubkey, zkvm_secp256r1_signature, zkvm_secp256r1_verify, +}; +pub use sha256::{zkvm_sha256, zkvm_sha256_hash}; +pub use types::{zkvm_status, ZKVM_EFAIL, ZKVM_EOK}; diff --git a/crates/accelerators/src/modexp.rs b/crates/accelerators/src/modexp.rs new file mode 100644 index 000000000..8319314e4 --- /dev/null +++ b/crates/accelerators/src/modexp.rs @@ -0,0 +1,139 @@ +//! Modular exponentiation accelerator. + +use alloc::{vec, vec::Vec}; + +use crate::types::{zkvm_status, ZKVM_EFAIL, ZKVM_EOK}; +use openvm_ecc_guest::algebra::{ExpBytes, IntMod, Reduce}; +use openvm_pairing::bn254 as bn; + +const BN_SCALAR_LEN: usize = 32; + +/// Compute `base^exp mod modulus`. +/// +/// # Safety +/// +/// Each non-empty input must be valid for its corresponding length, and `output` +/// must be valid for `mod_len` writes when `mod_len != 0`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn zkvm_modexp( + base: *const u8, + base_len: usize, + exp: *const u8, + exp_len: usize, + modulus: *const u8, + mod_len: usize, + output: *mut u8, +) -> zkvm_status { + if (base.is_null() && base_len != 0) || + (exp.is_null() && exp_len != 0) || + (modulus.is_null() && mod_len != 0) || + (output.is_null() && mod_len != 0) + { + return ZKVM_EFAIL; + } + + // SAFETY: non-NULL pointers and lengths were checked above; the caller guarantees validity. + let base = + if base_len == 0 { &[] } else { unsafe { core::slice::from_raw_parts(base, base_len) } }; + // SAFETY: see above. + let exp = if exp_len == 0 { &[] } else { unsafe { core::slice::from_raw_parts(exp, exp_len) } }; + // SAFETY: see above. + let modulus = + if mod_len == 0 { &[] } else { unsafe { core::slice::from_raw_parts(modulus, mod_len) } }; + let value = modexp(base, exp, modulus); + if mod_len != 0 { + // SAFETY: all input reads are complete and `output` is valid for `mod_len` writes. + unsafe { core::ptr::copy_nonoverlapping(value.as_ptr(), output, mod_len) }; + } + ZKVM_EOK +} + +fn modexp(base: &[u8], exp: &[u8], modulus: &[u8]) -> Vec { + let mut result = if is_bn254_fr(modulus) { + accelerated_modexp_bn254_fr(base, exp) + } else { + aurora_engine_modexp::modexp(base, exp, modulus) + }; + + let output_len = modulus.len(); + match result.len().cmp(&output_len) { + core::cmp::Ordering::Greater => { + let start = result.len() - output_len; + result.copy_within(start.., 0); + result.truncate(output_len); + } + core::cmp::Ordering::Less => { + let value_len = result.len(); + let padding = output_len - value_len; + result.resize(output_len, 0); + result.copy_within(0..value_len, padding); + result[..padding].fill(0); + } + core::cmp::Ordering::Equal => {} + } + result +} + +fn is_bn254_fr(modulus: &[u8]) -> bool { + let stripped = match modulus.iter().position(|&byte| byte != 0) { + Some(index) => &modulus[index..], + None => return false, + }; + stripped.len() == BN_SCALAR_LEN && stripped.iter().rev().eq(bn::Scalar::MODULUS.as_ref().iter()) +} + +fn accelerated_modexp_bn254_fr(base: &[u8], exp: &[u8]) -> Vec { + let padded_len = base.len().next_multiple_of(BN_SCALAR_LEN).max(BN_SCALAR_LEN); + let mut padded = vec![0u8; padded_len]; + padded[padded_len - base.len()..].copy_from_slice(base); + let base_fr = bn::Scalar::reduce_be_bytes(&padded); + base_fr.exp_bytes(true, exp).to_be_bytes().as_ref().to_vec() +} + +#[cfg(test)] +mod tests { + use super::*; + + const BN254_FR: [u8; 32] = [ + 0x30, 0x64, 0x4e, 0x72, 0xe1, 0x31, 0xa0, 0x29, 0xb8, 0x50, 0x45, 0xb6, 0x81, 0x81, 0x58, + 0x5d, 0x28, 0x33, 0xe8, 0x48, 0x79, 0xb9, 0x70, 0x91, 0x43, 0xe1, 0xf5, 0x93, 0xf0, 0x00, + 0x00, 0x01, + ]; + + fn check(base: &[u8], exp: &[u8]) { + let expected = aurora_engine_modexp::modexp(base, exp, &BN254_FR); + let actual = accelerated_modexp_bn254_fr(base, exp); + let mut expected_padded = vec![0u8; BN_SCALAR_LEN]; + let offset = BN_SCALAR_LEN - expected.len(); + expected_padded[offset..].copy_from_slice(&expected); + assert_eq!(actual, expected_padded, "base={base:?}, exp={exp:?}"); + } + + #[test] + fn recognizes_bn254_fr() { + assert!(is_bn254_fr(&BN254_FR)); + let mut padded = vec![0u8; 10]; + padded.extend_from_slice(&BN254_FR); + assert!(is_bn254_fr(&padded)); + assert!(!is_bn254_fr(&[0u8; 32])); + let mut wrong = BN254_FR; + *wrong.last_mut().unwrap() ^= 1; + assert!(!is_bn254_fr(&wrong)); + } + + #[test] + fn accelerated_bn254_fr_matches_software() { + for (base, exp) in [ + (&[3][..], &[5][..]), + (&[0], &[5]), + (&[3], &[0]), + (&[][..], &[][..]), + (&[0xff; 32], &[1]), + (&[0xab; 64], &[3]), + (&[0x42; 100], &[2]), + (&[2], &[0xff; 32]), + ] { + check(base, exp); + } + } +} diff --git a/crates/accelerators/src/ripemd160.rs b/crates/accelerators/src/ripemd160.rs new file mode 100644 index 000000000..339f7ded2 --- /dev/null +++ b/crates/accelerators/src/ripemd160.rs @@ -0,0 +1,42 @@ +//! RIPEMD-160 accelerator. + +use crate::types::{zkvm_status, ZkvmBytes, ZKVM_EFAIL, ZKVM_EOK}; + +pub type zkvm_ripemd160_hash = ZkvmBytes<32>; + +/// Compute the RIPEMD-160 hash of `data[..len]` into `output`. +/// +/// A NULL `data` pointer is accepted only when `len == 0`. +/// +/// # Safety +/// +/// - `data`, if non-NULL, must be valid for reads of `len` bytes. +/// - `output`, if non-NULL, must be valid for writes of one [`zkvm_ripemd160_hash`]. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn zkvm_ripemd160( + data: *const u8, + len: usize, + output: *mut zkvm_ripemd160_hash, +) -> zkvm_status { + if output.is_null() || (data.is_null() && len != 0) { + return ZKVM_EFAIL; + } + // SAFETY: non-NULL checked above; validity is guaranteed by the caller. + let data = if len == 0 { &[] } else { unsafe { core::slice::from_raw_parts(data, len) } }; + let value = zkvm_ripemd160_hash { data: ripemd160(data) }; + // SAFETY: `output` is non-NULL and valid for writes. All input reads are complete, so + // overlapping input/output storage is supported. + unsafe { output.write(value) }; + ZKVM_EOK +} + +#[inline] +fn ripemd160(data: &[u8]) -> [u8; 32] { + use ripemd::Digest; + let mut hasher = ripemd::Ripemd160::new(); + hasher.update(data); + + let mut output = [0; 32]; + hasher.finalize_into((&mut output[12..]).into()); + output +} diff --git a/crates/accelerators/src/secp256k1.rs b/crates/accelerators/src/secp256k1.rs new file mode 100644 index 000000000..bed812a20 --- /dev/null +++ b/crates/accelerators/src/secp256k1.rs @@ -0,0 +1,95 @@ +//! secp256k1 ECDSA accelerator. + +#[cfg(any(target_os = "none", target_os = "openvm"))] +use openvm_k256 as k256; + +use crate::types::{zkvm_status, ZkvmBytes, ZKVM_EFAIL, ZKVM_EOK}; +use k256::ecdsa::{signature::hazmat::PrehashVerifier, RecoveryId, Signature, VerifyingKey}; + +pub type zkvm_secp256k1_hash = ZkvmBytes<32>; +pub type zkvm_secp256k1_signature = ZkvmBytes<64>; +pub type zkvm_secp256k1_pubkey = ZkvmBytes<64>; + +/// Recover an uncompressed public key from a signature and recovery ID. +/// +/// # Safety +/// +/// Every non-NULL pointer must be valid for a read or write of its pointed-to type. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn zkvm_secp256k1_ecrecover( + msg: *const zkvm_secp256k1_hash, + sig: *const zkvm_secp256k1_signature, + recid: u8, + output: *mut zkvm_secp256k1_pubkey, +) -> zkvm_status { + if msg.is_null() || sig.is_null() || output.is_null() { + return ZKVM_EFAIL; + } + // SAFETY: the caller guarantees that the non-NULL inputs are valid for reads. Copying both + // inputs before writing supports overlap with `output`. + let (msg, sig) = unsafe { (msg.read(), sig.read()) }; + let Some(data) = recover(&msg.data, &sig.data, recid) else { + return ZKVM_EFAIL; + }; + // SAFETY: `output` is non-NULL and valid for writes. + unsafe { output.write(zkvm_secp256k1_pubkey { data }) }; + ZKVM_EOK +} + +/// Verify a signature, writing `false` for malformed or invalid cryptographic inputs. +/// +/// # Safety +/// +/// Every non-NULL pointer must be valid for a read or write of its pointed-to type. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn zkvm_secp256k1_verify( + msg: *const zkvm_secp256k1_hash, + sig: *const zkvm_secp256k1_signature, + pubkey: *const zkvm_secp256k1_pubkey, + verified: *mut bool, +) -> zkvm_status { + if msg.is_null() || sig.is_null() || pubkey.is_null() || verified.is_null() { + return ZKVM_EFAIL; + } + // SAFETY: the caller guarantees that the non-NULL inputs are valid for reads. Copying every + // input before writing supports overlap with `verified`. + let (msg, sig, pubkey) = unsafe { (msg.read(), sig.read(), pubkey.read()) }; + let value = verify(&msg.data, &sig.data, &pubkey.data); + // SAFETY: `verified` is non-NULL and valid for writes. + unsafe { verified.write(value) }; + ZKVM_EOK +} + +fn recover(msg: &[u8; 32], sig: &[u8; 64], mut recid: u8) -> Option<[u8; 64]> { + let mut signature = Signature::from_slice(sig).ok()?; + // k256 recovery requires low-s; changing s to -s also flips the recovery-ID parity. + if let Some(normalized) = signature.normalize_s() { + signature = normalized; + recid ^= 1; + } + let recovery_id = RecoveryId::from_byte(recid)?; + + #[cfg(any(target_os = "none", target_os = "openvm"))] + let key = VerifyingKey::recover_from_prehash_noverify(msg, &signature.to_bytes(), recovery_id) + .ok()?; + #[cfg(not(any(target_os = "none", target_os = "openvm")))] + let key = VerifyingKey::recover_from_prehash(msg, &signature, recovery_id).ok()?; + + key.to_encoded_point(false).as_bytes().get(1..65)?.try_into().ok() +} + +fn verify(msg: &[u8; 32], sig: &[u8; 64], pubkey: &[u8; 64]) -> bool { + let mut sec1 = [0u8; 65]; + sec1[0] = 0x04; + sec1[1..].copy_from_slice(pubkey); + let Ok(key) = VerifyingKey::from_sec1_bytes(&sec1) else { + return false; + }; + let Ok(mut signature) = Signature::from_slice(sig) else { + return false; + }; + if let Some(normalized) = signature.normalize_s() { + signature = normalized; + } + key.verify_prehash(msg, &signature).is_ok() +} diff --git a/crates/accelerators/src/secp256r1.rs b/crates/accelerators/src/secp256r1.rs new file mode 100644 index 000000000..d8ae7608f --- /dev/null +++ b/crates/accelerators/src/secp256r1.rs @@ -0,0 +1,46 @@ +//! secp256r1 ECDSA accelerator. + +use crate::types::{zkvm_status, ZkvmBytes, ZKVM_EFAIL, ZKVM_EOK}; +use openvm_p256::{ + ecdsa::{signature::hazmat::PrehashVerifier, Signature, VerifyingKey}, + EncodedPoint, +}; + +pub type zkvm_secp256r1_hash = ZkvmBytes<32>; +pub type zkvm_secp256r1_signature = ZkvmBytes<64>; +pub type zkvm_secp256r1_pubkey = ZkvmBytes<64>; + +/// Verify a signature, writing `false` for malformed or invalid cryptographic inputs. +/// +/// # Safety +/// +/// Every non-NULL pointer must be valid for a read or write of its pointed-to type. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn zkvm_secp256r1_verify( + msg: *const zkvm_secp256r1_hash, + sig: *const zkvm_secp256r1_signature, + pubkey: *const zkvm_secp256r1_pubkey, + verified: *mut bool, +) -> zkvm_status { + if msg.is_null() || sig.is_null() || pubkey.is_null() || verified.is_null() { + return ZKVM_EFAIL; + } + // SAFETY: the caller guarantees that the non-NULL inputs are valid for reads. Copying every + // input before writing supports overlap with `verified`. + let (msg, sig, pubkey) = unsafe { (msg.read(), sig.read(), pubkey.read()) }; + let value = verify(&msg.data, &sig.data, &pubkey.data); + // SAFETY: `verified` is non-NULL and valid for writes. + unsafe { verified.write(value) }; + ZKVM_EOK +} + +fn verify(msg: &[u8; 32], sig: &[u8; 64], pubkey: &[u8; 64]) -> bool { + let encoded_point = EncodedPoint::from_untagged_bytes(&(*pubkey).into()); + let Ok(key) = VerifyingKey::from_encoded_point(&encoded_point) else { + return false; + }; + let Ok(signature) = Signature::from_slice(sig) else { + return false; + }; + key.verify_prehash(msg, &signature).is_ok() +} diff --git a/crates/accelerators/src/sha256.rs b/crates/accelerators/src/sha256.rs new file mode 100644 index 000000000..4bb824bf2 --- /dev/null +++ b/crates/accelerators/src/sha256.rs @@ -0,0 +1,33 @@ +//! SHA-256 accelerator. + +use crate::types::{zkvm_status, ZkvmBytes, ZKVM_EFAIL, ZKVM_EOK}; +#[cfg(not(openvm_intrinsics))] +use openvm_sha2::Digest; + +pub type zkvm_sha256_hash = ZkvmBytes<32>; + +/// Compute the SHA-256 hash of `data[..len]` into `output`. +/// +/// A NULL `data` pointer is accepted only when `len == 0`. +/// +/// # Safety +/// +/// - `data`, if non-NULL, must be valid for reads of `len` bytes. +/// - `output`, if non-NULL, must be valid for writes of one [`zkvm_sha256_hash`]. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn zkvm_sha256( + data: *const u8, + len: usize, + output: *mut zkvm_sha256_hash, +) -> zkvm_status { + if output.is_null() || (data.is_null() && len != 0) { + return ZKVM_EFAIL; + } + // SAFETY: non-NULL checked above; validity is guaranteed by the caller. + let data = if len == 0 { &[] } else { unsafe { core::slice::from_raw_parts(data, len) } }; + let value = zkvm_sha256_hash { data: openvm_sha2::Sha256::digest(data).into() }; + // SAFETY: `output` is non-NULL and valid for writes. All input reads are complete, so + // overlapping input/output storage is supported. + unsafe { output.write(value) }; + ZKVM_EOK +} diff --git a/crates/accelerators/src/types.rs b/crates/accelerators/src/types.rs new file mode 100644 index 000000000..47cd563fc --- /dev/null +++ b/crates/accelerators/src/types.rs @@ -0,0 +1,12 @@ +//! Types for the standard zkVM accelerator C interface. + +pub type zkvm_status = core::ffi::c_int; + +pub const ZKVM_EOK: zkvm_status = 0; +pub const ZKVM_EFAIL: zkvm_status = -1; + +#[repr(C, align(8))] +#[derive(Clone, Copy, Debug)] +pub struct ZkvmBytes { + pub data: [u8; N], +} diff --git a/crates/accelerators/tests/blake2f.rs b/crates/accelerators/tests/blake2f.rs new file mode 100644 index 000000000..aeb287b3c --- /dev/null +++ b/crates/accelerators/tests/blake2f.rs @@ -0,0 +1,107 @@ +//! BLAKE2f conformance using the official EIP-152 test vectors 4-7. + +use hex_literal::hex; +use openvm_accelerators::{ + zkvm_blake2f, zkvm_blake2f_message, zkvm_blake2f_offset, zkvm_blake2f_state, ZKVM_EFAIL, + ZKVM_EOK, +}; + +/// EIP-152 vectors 4-7 share the same h, m and t inputs. +const H: [u8; 64] = hex!( + "48c9bdf267e6096a3ba7ca8485ae67bb2bf894fe72f36e3cf1361d5f3af54fa5" + "d182e6ad7f520e511f6c3e2b8c68059b6bbd41fbabd9831f79217e1319cde05b" +); +const T: [u8; 16] = hex!("03000000000000000000000000000000"); + +fn m() -> zkvm_blake2f_message { + let mut m = zkvm_blake2f_message { data: [0; 128] }; + m.data[..3].copy_from_slice(b"abc"); + m +} + +fn check(rounds: u32, f: u8, expected: [u8; 64]) { + let mut h = zkvm_blake2f_state { data: H }; + let m = m(); + let t = zkvm_blake2f_offset { data: T }; + + let status = unsafe { zkvm_blake2f(rounds, &mut h, &m, &t, f) }; + assert_eq!(status, ZKVM_EOK, "rounds={rounds}, f={f}"); + assert_eq!(h.data, expected, "rounds={rounds}, f={f}"); +} + +#[test] +fn blake2f_eip152_vector_4_zero_rounds() { + check( + 0, + 1, + hex!( + "08c9bcf367e6096a3ba7ca8485ae67bb2bf894fe72f36e3cf1361d5f3af54fa5" + "d282e6ad7f520e511f6c3e2b8c68059b9442be0454267ce079217e1319cde05b" + ), + ); +} + +#[test] +fn blake2f_eip152_vector_5_twelve_rounds() { + check( + 12, + 1, + hex!( + "ba80a53f981c4d0d6a2797b69f12f6e94c212f14685ac4b74b12bb6fdbffa2d1" + "7d87c5392aab792dc252d5de4533cc9518d38aa8dbf1925ab92386edd4009923" + ), + ); +} + +#[test] +fn blake2f_eip152_vector_6_no_final_flag() { + check( + 12, + 0, + hex!( + "75ab69d3190a562c51aef8d88f1c2775876944407270c42c9844252c26d28752" + "98743e7f6d5ea2f2d3e8d226039cd31b4e426ac4f2d3d666a610c2116fde4735" + ), + ); +} + +#[test] +fn blake2f_eip152_vector_7_one_round() { + check( + 1, + 1, + hex!( + "b63a380cb2897d521994a85234ee2c181b5f844d2c624c002677e9703449d2fb" + "a551b3a8333bcdf5f2f7e08993d53923de3d64fcc68c034e717b9293fed7a421" + ), + ); +} + +#[test] +fn zkvm_blake2f_invalid_final_flag_preserves_state() { + let mut h = zkvm_blake2f_state { data: H }; + let m = m(); + let t = zkvm_blake2f_offset { data: T }; + + let status = unsafe { zkvm_blake2f(12, &mut h, &m, &t, 2) }; + assert_eq!(status, ZKVM_EFAIL); + assert_eq!(h.data, H); +} + +#[test] +fn zkvm_blake2f_null_pointers() { + let mut h = zkvm_blake2f_state { data: H }; + let m = m(); + let t = zkvm_blake2f_offset { data: T }; + + let status = unsafe { zkvm_blake2f(12, core::ptr::null_mut(), &m, &t, 1) }; + assert_eq!(status, ZKVM_EFAIL); + + let status = unsafe { zkvm_blake2f(12, &mut h, core::ptr::null(), &t, 1) }; + assert_eq!(status, ZKVM_EFAIL); + + let status = unsafe { zkvm_blake2f(12, &mut h, &m, core::ptr::null(), 1) }; + assert_eq!(status, ZKVM_EFAIL); + // The state must be untouched when a pointer is NULL. + assert_eq!(h.data, H); +} diff --git a/crates/accelerators/tests/bls12_381.rs b/crates/accelerators/tests/bls12_381.rs new file mode 100644 index 000000000..906a90c5a --- /dev/null +++ b/crates/accelerators/tests/bls12_381.rs @@ -0,0 +1,334 @@ +//! BLS12-381 add/MSM/pairing/map conformance vectors. + +use hex_literal::hex; +use openvm_accelerators::{ + zkvm_bls12_381_fp, zkvm_bls12_381_fp2, zkvm_bls12_381_g1_msm_pair, zkvm_bls12_381_g1_point, + zkvm_bls12_381_g2_msm_pair, zkvm_bls12_381_g2_point, zkvm_bls12_381_pairing_pair, + zkvm_bls12_381_scalar, zkvm_bls12_g1_add, zkvm_bls12_g1_msm, zkvm_bls12_g2_add, + zkvm_bls12_g2_msm, zkvm_bls12_map_fp2_to_g2, zkvm_bls12_map_fp_to_g1, zkvm_bls12_pairing, + ZKVM_EFAIL, ZKVM_EOK, +}; + +fn scalar(value: u8) -> zkvm_bls12_381_scalar { + let mut scalar = zkvm_bls12_381_scalar { data: [0; 32] }; + scalar.data[31] = value; + scalar +} + +/// BLS12-381 G1 generator (`x || y`). +const BLS_G1_GEN: zkvm_bls12_381_g1_point = zkvm_bls12_381_g1_point { + data: hex!( + "17f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb" + "08b3f481e3aaa0f1a09e30ed741d8ae4fcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1" + ), +}; + +/// Doubled BLS12-381 G1 generator, stripped from the EIP-2537 test-vector padding. +const BLS_G1_2GEN: zkvm_bls12_381_g1_point = zkvm_bls12_381_g1_point { + data: hex!( + "0572cbea904d67468808c8eb50a9450c9721db309128012543902d0ac358a62ae28f75bb8f1c7c42c39a8c5529bf0f4e" + "166a9d8cabc673a322fda673779d8e3822ba3ecb8670e461f73bb9021d5fd76a4c56d9d4cd16bd1bba86881979749d28" + ), +}; + +/// BLS12-381 G2 generator in EIP-2537 order (`x_c0 || x_c1 || y_c0 || y_c1`). +const BLS_G2_GEN: zkvm_bls12_381_g2_point = zkvm_bls12_381_g2_point { + data: hex!( + "024aa2b2f08f0a91260805272dc51051c6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb8" + "13e02b6052719f607dacd3a088274f65596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e" + "0ce5d527727d6e118cc9cdc6da2e351aadfd9baa8cbdd3a76d429a695160d12c923ac9cc3baca289e193548608b82801" + "0606c4a02ea734cc32acd2b02bc28b99cb3e287e85a763af267492ab572e99ab3f370d275cec1da1aaa9075ff05f79be" + ), +}; + +/// Doubled BLS12-381 G2 generator, stripped from the EIP-2537 test-vector padding. +const BLS_G2_2GEN: zkvm_bls12_381_g2_point = zkvm_bls12_381_g2_point { + data: hex!( + "1638533957d540a9d2370f17cc7ed5863bc0b995b8825e0ee1ea1e1e4d00dbae81f14b0bf3611b78c952aacab827a053" + "0a4edef9c1ed7f729f520e47730a124fd70662a904ba1074728114d1031e1572c6c886f6b57ec72a6178288c47c33577" + "0468fb440d82b0630aeb8dca2b5256789a66da69bf91009cbfe6bd221e47aa8ae88dece9764bf3bd999d95d71e4c9899" + "0f6d4552fa65dd2638b361543f887136a43253d9c66c411697003f7a13c308f5422e1aa0a59c8967acdefd8b6e36ccf3" + ), +}; + +/// BLS12-381 scalar field order minus one; multiplying by it negates a point. +const BLS_R_MINUS_1: zkvm_bls12_381_scalar = zkvm_bls12_381_scalar { + data: hex!("73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000000"), +}; + +fn neg_g1_generator() -> zkvm_bls12_381_g1_point { + let pairs = [zkvm_bls12_381_g1_msm_pair { point: BLS_G1_GEN, scalar: BLS_R_MINUS_1 }]; + let mut output = zkvm_bls12_381_g1_point { data: [0; 96] }; + let status = unsafe { zkvm_bls12_g1_msm(pairs.as_ptr(), pairs.len(), &mut output) }; + assert_eq!(status, ZKVM_EOK); + output +} + +#[test] +fn bls12_g1_add_msm_vectors() { + let mut output = zkvm_bls12_381_g1_point { data: [0; 96] }; + let status = unsafe { zkvm_bls12_g1_add(&BLS_G1_GEN, &BLS_G1_GEN, &mut output) }; + assert_eq!(status, ZKVM_EOK); + assert_eq!(output.data, BLS_G1_2GEN.data); + + let pairs = [zkvm_bls12_381_g1_msm_pair { point: BLS_G1_GEN, scalar: scalar(2) }]; + output.data.fill(0); + let status = unsafe { zkvm_bls12_g1_msm(pairs.as_ptr(), pairs.len(), &mut output) }; + assert_eq!(status, ZKVM_EOK); + assert_eq!(output.data, BLS_G1_2GEN.data); +} + +#[test] +fn bls12_g2_add_msm_vectors() { + let mut output = zkvm_bls12_381_g2_point { data: [0; 192] }; + let status = unsafe { zkvm_bls12_g2_add(&BLS_G2_GEN, &BLS_G2_GEN, &mut output) }; + assert_eq!(status, ZKVM_EOK); + assert_eq!(output.data, BLS_G2_2GEN.data); + + let pairs = [zkvm_bls12_381_g2_msm_pair { point: BLS_G2_GEN, scalar: scalar(2) }]; + output.data.fill(0); + let status = unsafe { zkvm_bls12_g2_msm(pairs.as_ptr(), pairs.len(), &mut output) }; + assert_eq!(status, ZKVM_EOK); + assert_eq!(output.data, BLS_G2_2GEN.data); +} + +#[test] +fn bls12_pairing_vectors() { + let neg_g1 = neg_g1_generator(); + let pairs = [ + zkvm_bls12_381_pairing_pair { g1: BLS_G1_GEN, g2: BLS_G2_GEN }, + zkvm_bls12_381_pairing_pair { g1: neg_g1, g2: BLS_G2_GEN }, + ]; + let mut verified = false; + + let status = unsafe { zkvm_bls12_pairing(pairs.as_ptr(), pairs.len(), &mut verified) }; + assert_eq!(status, ZKVM_EOK); + assert!(verified); + + let status = unsafe { zkvm_bls12_pairing(pairs.as_ptr(), 1, &mut verified) }; + assert_eq!(status, ZKVM_EOK); + assert!(!verified); +} + +#[test] +fn bls12_rejects_invalid_points() { + let mut off_curve_g1 = BLS_G1_GEN; + off_curve_g1.data[95] ^= 1; + let mut g1_output = zkvm_bls12_381_g1_point { data: [0; 96] }; + let status = unsafe { zkvm_bls12_g1_add(&off_curve_g1, &BLS_G1_GEN, &mut g1_output) }; + assert_eq!(status, ZKVM_EFAIL); + + let mut off_curve_g2 = BLS_G2_GEN; + off_curve_g2.data[191] ^= 1; + let mut g2_output = zkvm_bls12_381_g2_point { data: [0; 192] }; + let status = unsafe { zkvm_bls12_g2_add(&off_curve_g2, &BLS_G2_GEN, &mut g2_output) }; + assert_eq!(status, ZKVM_EFAIL); + + let pairs = [zkvm_bls12_381_pairing_pair { g1: off_curve_g1, g2: BLS_G2_GEN }]; + let mut verified = true; + let status = unsafe { zkvm_bls12_pairing(pairs.as_ptr(), pairs.len(), &mut verified) }; + assert_eq!(status, ZKVM_EFAIL); + assert!(verified); +} + +#[test] +fn zkvm_bls12_null_pointers() { + let mut g1_output = zkvm_bls12_381_g1_point { data: [0; 96] }; + let status = unsafe { zkvm_bls12_g1_add(core::ptr::null(), &BLS_G1_GEN, &mut g1_output) }; + assert_eq!(status, ZKVM_EFAIL); + + let status = unsafe { zkvm_bls12_g1_add(&BLS_G1_GEN, &BLS_G1_GEN, core::ptr::null_mut()) }; + assert_eq!(status, ZKVM_EFAIL); + + g1_output.data.fill(0xff); + let status = unsafe { zkvm_bls12_g1_msm(core::ptr::null(), 0, &mut g1_output) }; + assert_eq!(status, ZKVM_EOK); + assert_eq!(g1_output.data, [0u8; 96]); + + let status = unsafe { zkvm_bls12_g1_msm(core::ptr::null(), 1, &mut g1_output) }; + assert_eq!(status, ZKVM_EFAIL); + + let mut g2_output = zkvm_bls12_381_g2_point { data: [0; 192] }; + let status = unsafe { zkvm_bls12_g2_add(core::ptr::null(), &BLS_G2_GEN, &mut g2_output) }; + assert_eq!(status, ZKVM_EFAIL); + + let status = unsafe { zkvm_bls12_g2_add(&BLS_G2_GEN, &BLS_G2_GEN, core::ptr::null_mut()) }; + assert_eq!(status, ZKVM_EFAIL); + + g2_output.data.fill(0xff); + let status = unsafe { zkvm_bls12_g2_msm(core::ptr::null(), 0, &mut g2_output) }; + assert_eq!(status, ZKVM_EOK); + assert_eq!(g2_output.data, [0u8; 192]); + + let status = unsafe { zkvm_bls12_g2_msm(core::ptr::null(), 1, &mut g2_output) }; + assert_eq!(status, ZKVM_EFAIL); + + let pairs = [zkvm_bls12_381_pairing_pair { g1: BLS_G1_GEN, g2: BLS_G2_GEN }]; + let mut verified = false; + + let status = unsafe { zkvm_bls12_pairing(core::ptr::null(), 0, &mut verified) }; + assert_eq!(status, ZKVM_EOK); + assert!(verified); + + let status = unsafe { zkvm_bls12_pairing(core::ptr::null(), 1, &mut verified) }; + assert_eq!(status, ZKVM_EFAIL); + + let status = unsafe { zkvm_bls12_pairing(pairs.as_ptr(), pairs.len(), core::ptr::null_mut()) }; + assert_eq!(status, ZKVM_EFAIL); +} + +/* ============================================================================ + * Map to curve + * ============================================================================ */ + +/// Official EIP-2537 vectors for MAP_FP_TO_G1, from https://github.com/ethereum/EIPs/blob/master/assets/eip-2537/map_fp_to_G1_bls.json +const MAP_FP_TO_G1_VECTORS: [([u8; 48], [u8; 96]); 2] = [ + // "bls_g1map_" + ( + hex!("156c8a6a2c184569d69a76be144b5cdc5141d2d2ca4fe341f011e25e3969c55ad9e9b9ce2eb833c81a908e5fa4ac5f03"), + hex!( + "184bb665c37ff561a89ec2122dd343f20e0f4cbcaec84e3c3052ea81d1834e192c426074b02ed3dca4e7676ce4ce48ba" + "04407b8d35af4dacc809927071fc0405218f1401a6d15af775810e4e460064bcc9468beeba82fdc751be70476c888bf3" + ), + ), + // "bls_g1map_616263" + ( + hex!("147e1ed29f06e4c5079b9d14fc89d2820d32419b990c1c7bb7dbea2a36a045124b31ffbde7c99329c05c559af1c6cc82"), + hex!( + "009769f3ab59bfd551d53a5f846b9984c59b97d6842b20a2c565baa167945e3d026a3755b6345df8ec7e6acb6868ae6d" + "1532c00cf61aa3d0ce3e5aa20c3b531a2abd2c770a790a2613818303c6b830ffc0ecf6c357af3317b9575c567f11cd2c" + ), + ), +]; + +/// Official EIP-2537 vectors for MAP_FP2_TO_G2, from https://github.com/ethereum/EIPs/blob/master/assets/eip-2537/map_fp2_to_G2_bls.json +const MAP_FP2_TO_G2_VECTORS: [([u8; 96], [u8; 192]); 2] = [ + // "bls_g2map_" + ( + hex!( + "07355d25caf6e7f2f0cb2812ca0e513bd026ed09dda65b177500fa31714e09ea0ded3a078b526bed3307f804d4b93b04" + "02829ce3c021339ccb5caf3e187f6370e1e2a311dec9b75363117063ab2015603ff52c3d3b98f19c2f65575e99e8b78c" + ), + hex!( + "00e7f4568a82b4b7dc1f14c6aaa055edf51502319c723c4dc2688c7fe5944c213f510328082396515734b6612c4e7bb7" + "126b855e9e69b1f691f816e48ac6977664d24d99f8724868a184186469ddfd4617367e94527d4b74fc86413483afb35b" + "0caead0fd7b6176c01436833c79d305c78be307da5f6af6c133c47311def6ff1e0babf57a0fb5539fce7ee12407b0a42" + "1498aadcf7ae2b345243e281ae076df6de84455d766ab6fcdaad71fab60abb2e8b980a440043cd305db09d283c895e3d" + ), + ), + // "bls_g2map_616263" + ( + hex!( + "138879a9559e24cecee8697b8b4ad32cced053138ab913b99872772dc753a2967ed50aabc907937aefb2439ba06cc50c" + "0a1ae7999ea9bab1dcc9ef8887a6cb6e8f1e22566015428d220b7eec90ffa70ad1f624018a9ad11e78d588bd3617f9f2" + ), + hex!( + "108ed59fd9fae381abfd1d6bce2fd2fa220990f0f837fa30e0f27914ed6e1454db0d1ee957b219f61da6ff8be0d6441f" + "0296238ea82c6d4adb3c838ee3cb2346049c90b96d602d7bb1b469b905c9228be25c627bffee872def773d5b2a2eb57d" + "033f90f6057aadacae7963b0a0b379dd46750c1c94a6357c99b65f63b79e321ff50fe3053330911c56b6ceea08fee656" + "153606c417e59fb331b7ae6bce4fbf7c5190c33ce9402b5ebe2b70e44fca614f3f1382a3625ed5493843d0b0a652fc3f" + ), + ), +]; + +/// The largest canonical field element. +const BLS_FP_MAX: [u8; 48] = + hex!("1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa"); + +/// The base field modulus itself, which is not a canonical field element. +const BLS_FP_MODULUS: [u8; 48] = + hex!("1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab"); + +#[test] +fn bls12_map_fp_to_g1_vectors() { + for (input, expected) in MAP_FP_TO_G1_VECTORS { + let field_element = zkvm_bls12_381_fp { data: input }; + let mut output = zkvm_bls12_381_g1_point { data: [0; 96] }; + let status = unsafe { zkvm_bls12_map_fp_to_g1(&field_element, &mut output) }; + assert_eq!(status, ZKVM_EOK, "input={input:?}"); + assert_eq!(output.data, expected, "input={input:?}"); + } +} + +#[test] +fn bls12_map_fp2_to_g2_vectors() { + for (input, expected) in MAP_FP2_TO_G2_VECTORS { + let field_element = zkvm_bls12_381_fp2 { data: input }; + let mut output = zkvm_bls12_381_g2_point { data: [0; 192] }; + let status = unsafe { zkvm_bls12_map_fp2_to_g2(&field_element, &mut output) }; + assert_eq!(status, ZKVM_EOK, "input={input:?}"); + assert_eq!(output.data, expected, "input={input:?}"); + } +} + +/// Mapped points must be valid members of the prime-order subgroup. +/// +/// MSM re-parses the point and validates subgroup membership, so this is an +/// independent check that the cofactor was cleared; multiplying by one must +/// return the point itself. +#[test] +fn bls12_map_lands_in_prime_order_subgroup() { + let fp = zkvm_bls12_381_fp { data: MAP_FP_TO_G1_VECTORS[0].0 }; + let mut mapped_g1 = zkvm_bls12_381_g1_point { data: [0; 96] }; + let status = unsafe { zkvm_bls12_map_fp_to_g1(&fp, &mut mapped_g1) }; + assert_eq!(status, ZKVM_EOK); + + let pairs = [zkvm_bls12_381_g1_msm_pair { point: mapped_g1, scalar: scalar(1) }]; + let mut output_g1 = zkvm_bls12_381_g1_point { data: [0; 96] }; + let status = unsafe { zkvm_bls12_g1_msm(pairs.as_ptr(), pairs.len(), &mut output_g1) }; + assert_eq!(status, ZKVM_EOK, "mapped G1 point must pass the subgroup check"); + assert_eq!(output_g1.data, mapped_g1.data); + + let fp2 = zkvm_bls12_381_fp2 { data: MAP_FP2_TO_G2_VECTORS[0].0 }; + let mut mapped_g2 = zkvm_bls12_381_g2_point { data: [0; 192] }; + let status = unsafe { zkvm_bls12_map_fp2_to_g2(&fp2, &mut mapped_g2) }; + assert_eq!(status, ZKVM_EOK); + + let pairs = [zkvm_bls12_381_g2_msm_pair { point: mapped_g2, scalar: scalar(1) }]; + let mut output_g2 = zkvm_bls12_381_g2_point { data: [0; 192] }; + let status = unsafe { zkvm_bls12_g2_msm(pairs.as_ptr(), pairs.len(), &mut output_g2) }; + assert_eq!(status, ZKVM_EOK, "mapped G2 point must pass the subgroup check"); + assert_eq!(output_g2.data, mapped_g2.data); +} + +#[test] +fn bls12_map_field_element_range() { + // The largest canonical element is accepted, the modulus itself is not. + let mut output = zkvm_bls12_381_g1_point { data: [0; 96] }; + let field_element = zkvm_bls12_381_fp { data: BLS_FP_MAX }; + let status = unsafe { zkvm_bls12_map_fp_to_g1(&field_element, &mut output) }; + assert_eq!(status, ZKVM_EOK); + + for input in [BLS_FP_MODULUS, [0xff; 48]] { + let field_element = zkvm_bls12_381_fp { data: input }; + let status = unsafe { zkvm_bls12_map_fp_to_g1(&field_element, &mut output) }; + assert_eq!(status, ZKVM_EFAIL); + } + + // Either half of an Fp2 input is checked. + let mut g2 = zkvm_bls12_381_g2_point { data: [0; 192] }; + for modulus_offset in [0, 48] { + let mut input = [0; 96]; + input[modulus_offset..modulus_offset + 48].copy_from_slice(&BLS_FP_MODULUS); + let field_element = zkvm_bls12_381_fp2 { data: input }; + let status = unsafe { zkvm_bls12_map_fp2_to_g2(&field_element, &mut g2) }; + assert_eq!(status, ZKVM_EFAIL); + } +} + +#[test] +fn zkvm_bls12_map_null_pointers() { + let field_element = zkvm_bls12_381_fp { data: MAP_FP_TO_G1_VECTORS[0].0 }; + let mut g1 = zkvm_bls12_381_g1_point { data: [0; 96] }; + let status = unsafe { zkvm_bls12_map_fp_to_g1(core::ptr::null(), &mut g1) }; + assert_eq!(status, ZKVM_EFAIL); + let status = unsafe { zkvm_bls12_map_fp_to_g1(&field_element, core::ptr::null_mut()) }; + assert_eq!(status, ZKVM_EFAIL); + + let field_element = zkvm_bls12_381_fp2 { data: MAP_FP2_TO_G2_VECTORS[0].0 }; + let mut g2 = zkvm_bls12_381_g2_point { data: [0; 192] }; + let status = unsafe { zkvm_bls12_map_fp2_to_g2(core::ptr::null(), &mut g2) }; + assert_eq!(status, ZKVM_EFAIL); + let status = unsafe { zkvm_bls12_map_fp2_to_g2(&field_element, core::ptr::null_mut()) }; + assert_eq!(status, ZKVM_EFAIL); +} diff --git a/crates/accelerators/tests/bn254.rs b/crates/accelerators/tests/bn254.rs new file mode 100644 index 000000000..44b975748 --- /dev/null +++ b/crates/accelerators/tests/bn254.rs @@ -0,0 +1,133 @@ +//! BN254 add/mul/pairing conformance vectors. + +use hex_literal::hex; +use openvm_accelerators::{ + zkvm_bn254_g1_add, zkvm_bn254_g1_mul, zkvm_bn254_g1_point, zkvm_bn254_g2_point, + zkvm_bn254_pairing, zkvm_bn254_pairing_pair, zkvm_bn254_scalar, ZKVM_EFAIL, ZKVM_EOK, +}; + +fn scalar(value: u8) -> zkvm_bn254_scalar { + let mut scalar = zkvm_bn254_scalar { data: [0; 32] }; + scalar.data[31] = value; + scalar +} + +/// BN254 generator (1, 2). +fn generator() -> zkvm_bn254_g1_point { + let mut point = zkvm_bn254_g1_point { data: [0; 64] }; + point.data[31] = 1; + point.data[63] = 2; + point +} + +/// Doubled BN254 generator, from the EIP-196 reference vectors. +const BN254_2GEN: zkvm_bn254_g1_point = zkvm_bn254_g1_point { + data: hex!( + "030644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd3" + "15ed738c0e0a7c92e7845f96b2ae9c0a68a6a449e3538fc7ff3ebf7a5a18a2c4" + ), +}; + +/// BN254 negated generator (1, p - 2). +const BN254_NEG_GEN: zkvm_bn254_g1_point = zkvm_bn254_g1_point { + data: hex!( + "0000000000000000000000000000000000000000000000000000000000000001" + "30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd45" + ), +}; + +/// BN254 G2 generator in EIP-197 order (`x_c1 || x_c0 || y_c1 || y_c0`). +const BN254_G2_GEN: zkvm_bn254_g2_point = zkvm_bn254_g2_point { + data: hex!( + "198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c2" + "1800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed" + "090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b" + "12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa" + ), +}; + +#[test] +fn zkvm_bn254_add_mul_smoke() { + let point = generator(); + let mut output = zkvm_bn254_g1_point { data: [0; 64] }; + + let status = unsafe { zkvm_bn254_g1_add(&point, &point, &mut output) }; + assert_eq!(status, ZKVM_EOK); + assert_eq!(output.data, BN254_2GEN.data); + + output.data = [0; 64]; + let status = unsafe { zkvm_bn254_g1_mul(&point, &scalar(2), &mut output) }; + assert_eq!(status, ZKVM_EOK); + assert_eq!(output.data, BN254_2GEN.data); +} + +#[test] +fn zkvm_bn254_pairing_smoke() { + let pairs = [ + zkvm_bn254_pairing_pair { g1: generator(), g2: BN254_G2_GEN }, + zkvm_bn254_pairing_pair { g1: BN254_NEG_GEN, g2: BN254_G2_GEN }, + ]; + let mut verified = false; + + let status = unsafe { zkvm_bn254_pairing(pairs.as_ptr(), pairs.len(), &mut verified) }; + assert_eq!(status, ZKVM_EOK); + assert!(verified); + + let status = unsafe { zkvm_bn254_pairing(pairs.as_ptr(), 1, &mut verified) }; + assert_eq!(status, ZKVM_EOK); + assert!(!verified); +} + +#[test] +fn bn254_rejects_invalid_point() { + let mut not_on_curve = generator(); + not_on_curve.data[63] = 3; + let mut output = zkvm_bn254_g1_point { data: [0; 64] }; + + let status = unsafe { zkvm_bn254_g1_mul(¬_on_curve, &scalar(2), &mut output) }; + assert_eq!(status, ZKVM_EFAIL); + + let pairs = [zkvm_bn254_pairing_pair { g1: not_on_curve, g2: BN254_G2_GEN }]; + let mut verified = true; + let status = unsafe { zkvm_bn254_pairing(pairs.as_ptr(), pairs.len(), &mut verified) }; + assert_eq!(status, ZKVM_EFAIL); + assert!(verified); +} + +#[test] +fn zkvm_bn254_null_pointers() { + let point = generator(); + let scalar = scalar(2); + let mut output = zkvm_bn254_g1_point { data: [0; 64] }; + + let status = unsafe { zkvm_bn254_g1_add(core::ptr::null(), &point, &mut output) }; + assert_eq!(status, ZKVM_EFAIL); + + let status = unsafe { zkvm_bn254_g1_add(&point, core::ptr::null(), &mut output) }; + assert_eq!(status, ZKVM_EFAIL); + + let status = unsafe { zkvm_bn254_g1_add(&point, &point, core::ptr::null_mut()) }; + assert_eq!(status, ZKVM_EFAIL); + + let status = unsafe { zkvm_bn254_g1_mul(core::ptr::null(), &scalar, &mut output) }; + assert_eq!(status, ZKVM_EFAIL); + + let status = unsafe { zkvm_bn254_g1_mul(&point, core::ptr::null(), &mut output) }; + assert_eq!(status, ZKVM_EFAIL); + + let status = unsafe { zkvm_bn254_g1_mul(&point, &scalar, core::ptr::null_mut()) }; + assert_eq!(status, ZKVM_EFAIL); + + let pairs = [zkvm_bn254_pairing_pair { g1: point, g2: BN254_G2_GEN }]; + let mut verified = false; + + let status = unsafe { zkvm_bn254_pairing(core::ptr::null(), 0, &mut verified) }; + assert_eq!(status, ZKVM_EOK); + assert!(verified); + + let status = unsafe { zkvm_bn254_pairing(core::ptr::null(), 1, &mut verified) }; + assert_eq!(status, ZKVM_EFAIL); + + let status = unsafe { zkvm_bn254_pairing(pairs.as_ptr(), pairs.len(), core::ptr::null_mut()) }; + assert_eq!(status, ZKVM_EFAIL); +} diff --git a/crates/accelerators/tests/keccak256.rs b/crates/accelerators/tests/keccak256.rs new file mode 100644 index 000000000..db11e8a4e --- /dev/null +++ b/crates/accelerators/tests/keccak256.rs @@ -0,0 +1,36 @@ +//! Keccak-256 C-interface conformance vectors. + +use hex_literal::hex; +use openvm_accelerators::{zkvm_keccak256, zkvm_keccak256_hash, ZKVM_EFAIL, ZKVM_EOK}; + +#[test] +fn keccak256_abc() { + let data = *b"abc"; + let mut output = zkvm_keccak256_hash { data: [0; 32] }; + let status = unsafe { zkvm_keccak256(data.as_ptr(), data.len(), &mut output) }; + assert_eq!(status, ZKVM_EOK); + assert_eq!( + output.data, + hex!("4e03657aea45a94fc7d47ba826c8d667c0d1e6e33a64a036ec44f58fa12d6c45") + ); +} + +#[test] +fn keccak256_null_pointers() { + let data = *b"abc"; + let mut output = zkvm_keccak256_hash { data: [0; 32] }; + + // A NULL `data` with `len == 0` is the empty input. + let status = unsafe { zkvm_keccak256(core::ptr::null(), 0, &mut output) }; + assert_eq!(status, ZKVM_EOK); + assert_eq!( + output.data, + hex!("c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470") + ); + + let status = unsafe { zkvm_keccak256(core::ptr::null(), data.len(), &mut output) }; + assert_eq!(status, ZKVM_EFAIL); + + let status = unsafe { zkvm_keccak256(data.as_ptr(), data.len(), core::ptr::null_mut()) }; + assert_eq!(status, ZKVM_EFAIL); +} diff --git a/crates/accelerators/tests/kzg.rs b/crates/accelerators/tests/kzg.rs new file mode 100644 index 000000000..d4fb894a8 --- /dev/null +++ b/crates/accelerators/tests/kzg.rs @@ -0,0 +1,76 @@ +//! KZG point-evaluation conformance. + +use hex_literal::hex; +use openvm_accelerators::{ + zkvm_kzg_commitment, zkvm_kzg_field_element, zkvm_kzg_point_eval, zkvm_kzg_proof, ZKVM_EFAIL, + ZKVM_EOK, +}; + +// ethereum/consensus-spec-tests: +// verify_kzg_proof_case_correct_proof_1ce8e4f69d5df899. +const COMMITMENT: [u8; 48] = + hex!("93efc82d2017e9c57834a1246463e64774e56183bb247c8fc9dd98c56817e878d97b05f5c8d900acf1fbbbca6f146556"); +const Z: [u8; 32] = hex!("73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000000"); +const Y: [u8; 32] = [0; 32]; +const PROOF: [u8; 48] = + hex!("92c51ff81dd71dab71cefecd79e8274b4b7ba36a0f40e2dc086bc4061c7f63249877db23297212991fd63e07b7ebc348"); + +/// The compressed point at infinity: 0xc0 followed by zeros. +fn infinity() -> zkvm_kzg_commitment { + let mut point = zkvm_kzg_commitment { data: [0; 48] }; + point.data[0] = 0xc0; + point +} + +fn scalar(value: u8) -> zkvm_kzg_field_element { + let mut s = zkvm_kzg_field_element { data: [0; 32] }; + s.data[31] = value; + s +} + +#[test] +fn zkvm_kzg_point_eval_smoke() { + let commitment = zkvm_kzg_commitment { data: COMMITMENT }; + let proof = zkvm_kzg_proof { data: PROOF }; + let z = zkvm_kzg_field_element { data: Z }; + let y = zkvm_kzg_field_element { data: Y }; + let mut verified = false; + + let status = unsafe { zkvm_kzg_point_eval(&commitment, &z, &y, &proof, &mut verified) }; + assert_eq!(status, ZKVM_EOK); + assert!(verified); + + // Malformed cryptographic inputs are a completed verification with a false result. + let mut garbage = zkvm_kzg_commitment { data: [0; 48] }; + garbage.data[0] = 0x01; + let status = unsafe { zkvm_kzg_point_eval(&garbage, &z, &y, &proof, &mut verified) }; + assert_eq!(status, ZKVM_EOK); + assert!(!verified); +} + +#[test] +fn zkvm_kzg_point_eval_null_pointers() { + let commitment = infinity(); + let proof: zkvm_kzg_proof = infinity(); + let z = scalar(2); + let y = scalar(0); + let mut verified = false; + + let status = unsafe { zkvm_kzg_point_eval(core::ptr::null(), &z, &y, &proof, &mut verified) }; + assert_eq!(status, ZKVM_EFAIL); + + let status = + unsafe { zkvm_kzg_point_eval(&commitment, core::ptr::null(), &y, &proof, &mut verified) }; + assert_eq!(status, ZKVM_EFAIL); + + let status = + unsafe { zkvm_kzg_point_eval(&commitment, &z, core::ptr::null(), &proof, &mut verified) }; + assert_eq!(status, ZKVM_EFAIL); + + let status = + unsafe { zkvm_kzg_point_eval(&commitment, &z, &y, core::ptr::null(), &mut verified) }; + assert_eq!(status, ZKVM_EFAIL); + + let status = unsafe { zkvm_kzg_point_eval(&commitment, &z, &y, &proof, core::ptr::null_mut()) }; + assert_eq!(status, ZKVM_EFAIL); +} diff --git a/crates/accelerators/tests/modexp.rs b/crates/accelerators/tests/modexp.rs new file mode 100644 index 000000000..d47f4acfb --- /dev/null +++ b/crates/accelerators/tests/modexp.rs @@ -0,0 +1,97 @@ +//! Modexp conformance vectors. + +use hex_literal::hex; +use openvm_accelerators::{zkvm_modexp, ZKVM_EFAIL, ZKVM_EOK}; + +/// BN254 Fr (the scalar field) modulus, big-endian. Not to be confused with +/// the base field prime, which shares the leading bytes. +const BN254_FR: [u8; 32] = hex!("30644e72e131a029b85045b68181585d2833e84879b9709143e1f593f0000001"); + +#[test] +fn bn254_fr_accelerated_path_matches_reference() { + let base = [0xab; 32]; + let exp = [0x07]; + let mut output = [0; 32]; + let status = unsafe { + zkvm_modexp( + base.as_ptr(), + base.len(), + exp.as_ptr(), + exp.len(), + BN254_FR.as_ptr(), + BN254_FR.len(), + output.as_mut_ptr(), + ) + }; + assert_eq!(status, ZKVM_EOK); + let reference = aurora_engine_modexp::modexp(&[0xab; 32], &[0x07], &BN254_FR); + let mut expected = [0; 32]; + expected[32 - reference.len()..].copy_from_slice(&reference); + assert_eq!(output, expected); +} + +#[test] +fn zkvm_modexp_smoke() { + // 3^5 mod 7 = 5 + let base = [3u8]; + let exp = [5u8]; + let modulus = [7u8]; + let mut output = [0xffu8; 1]; + let status = unsafe { + zkvm_modexp(base.as_ptr(), 1, exp.as_ptr(), 1, modulus.as_ptr(), 1, output.as_mut_ptr()) + }; + assert_eq!(status, ZKVM_EOK); + assert_eq!(output, [5]); +} + +#[test] +fn zkvm_modexp_null_pointers() { + let base = [3u8]; + let exp = [5u8]; + let modulus = [7u8]; + let mut output = [0xffu8; 1]; + + // NULL base and exp with zero lengths are empty inputs: 0^0 mod 7 = 1. + let status = unsafe { + zkvm_modexp( + core::ptr::null(), + 0, + core::ptr::null(), + 0, + modulus.as_ptr(), + 1, + output.as_mut_ptr(), + ) + }; + assert_eq!(status, ZKVM_EOK); + assert_eq!(output, [1]); + + // A NULL pointer with a non-zero length fails. + let status = unsafe { + zkvm_modexp(core::ptr::null(), 1, exp.as_ptr(), 1, modulus.as_ptr(), 1, output.as_mut_ptr()) + }; + assert_eq!(status, ZKVM_EFAIL); + + let status = unsafe { + zkvm_modexp( + base.as_ptr(), + 1, + core::ptr::null(), + 1, + modulus.as_ptr(), + 1, + output.as_mut_ptr(), + ) + }; + assert_eq!(status, ZKVM_EFAIL); + + let status = unsafe { + zkvm_modexp(base.as_ptr(), 1, exp.as_ptr(), 1, core::ptr::null(), 1, output.as_mut_ptr()) + }; + assert_eq!(status, ZKVM_EFAIL); + + let status = unsafe { + zkvm_modexp(base.as_ptr(), 1, exp.as_ptr(), 1, modulus.as_ptr(), 1, core::ptr::null_mut()) + }; + assert_eq!(status, ZKVM_EFAIL); +} diff --git a/crates/accelerators/tests/ripemd160.rs b/crates/accelerators/tests/ripemd160.rs new file mode 100644 index 000000000..502eb8aa1 --- /dev/null +++ b/crates/accelerators/tests/ripemd160.rs @@ -0,0 +1,36 @@ +//! RIPEMD-160 C-interface conformance vectors. + +use hex_literal::hex; +use openvm_accelerators::{zkvm_ripemd160, zkvm_ripemd160_hash, ZKVM_EFAIL, ZKVM_EOK}; + +#[test] +fn ripemd160_abc() { + let data = *b"abc"; + let mut output = zkvm_ripemd160_hash { data: [0xff; 32] }; + let status = unsafe { zkvm_ripemd160(data.as_ptr(), data.len(), &mut output) }; + assert_eq!(status, ZKVM_EOK); + assert_eq!( + output.data, + hex!("0000000000000000000000008eb208f7e05d987a9b044a8e98c6b087f15a0bfc") + ); +} + +#[test] +fn ripemd160_null_pointers() { + let data = *b"abc"; + let mut output = zkvm_ripemd160_hash { data: [0xff; 32] }; + + // A NULL `data` with `len == 0` is the empty input. + let status = unsafe { zkvm_ripemd160(core::ptr::null(), 0, &mut output) }; + assert_eq!(status, ZKVM_EOK); + assert_eq!( + output.data, + hex!("0000000000000000000000009c1185a5c5e9fc54612808977ee8f548b2258d31") + ); + + let status = unsafe { zkvm_ripemd160(core::ptr::null(), data.len(), &mut output) }; + assert_eq!(status, ZKVM_EFAIL); + + let status = unsafe { zkvm_ripemd160(data.as_ptr(), data.len(), core::ptr::null_mut()) }; + assert_eq!(status, ZKVM_EFAIL); +} diff --git a/crates/accelerators/tests/secp256k1.rs b/crates/accelerators/tests/secp256k1.rs new file mode 100644 index 000000000..6c8573fc6 --- /dev/null +++ b/crates/accelerators/tests/secp256k1.rs @@ -0,0 +1,103 @@ +//! secp256k1 C-interface conformance vectors. + +use hex_literal::hex; +use openvm_accelerators::{ + zkvm_keccak256, zkvm_keccak256_hash, zkvm_secp256k1_ecrecover, zkvm_secp256k1_hash, + zkvm_secp256k1_pubkey, zkvm_secp256k1_signature, zkvm_secp256k1_verify, ZKVM_EFAIL, ZKVM_EOK, +}; + +const MSG: zkvm_secp256k1_hash = zkvm_secp256k1_hash { + data: hex!("456e9aea5e197a1f1af7a3e85a3212fa4049a3ba34c2289b4c860fc0b0c64ef3"), +}; +const SIG: zkvm_secp256k1_signature = zkvm_secp256k1_signature { + data: hex!( + "9242685bf161793cc25603c231bc2f568eb630ea16aa137d2664ac8038825608" + "4f8ae3bd7535248d0bd448298cc2e2071e56992d0774dc340c368ae950852ada" + ), +}; +const ADDRESS: [u8; 20] = hex!("7156526fbd7a3c72969b54f64e42c10fbb768c8a"); + +#[test] +fn secp256k1_ecrecover_vector() { + let mut pubkey = zkvm_secp256k1_pubkey { data: [0; 64] }; + let status = unsafe { zkvm_secp256k1_ecrecover(&MSG, &SIG, 1, &mut pubkey) }; + assert_eq!(status, ZKVM_EOK); + + let mut digest = zkvm_keccak256_hash { data: [0; 32] }; + let status = unsafe { zkvm_keccak256(pubkey.data.as_ptr(), pubkey.data.len(), &mut digest) }; + assert_eq!(status, ZKVM_EOK); + assert_eq!(digest.data[12..], ADDRESS); +} + +#[test] +fn secp256k1_ecrecover_invalid_inputs() { + let unchanged = [0x55; 64]; + let mut pubkey = zkvm_secp256k1_pubkey { data: unchanged }; + + let status = unsafe { zkvm_secp256k1_ecrecover(&MSG, &SIG, 4, &mut pubkey) }; + assert_eq!(status, ZKVM_EFAIL); + assert_eq!(pubkey.data, unchanged); + + let zero_sig = zkvm_secp256k1_signature { data: [0; 64] }; + let status = unsafe { zkvm_secp256k1_ecrecover(&MSG, &zero_sig, 0, &mut pubkey) }; + assert_eq!(status, ZKVM_EFAIL); + assert_eq!(pubkey.data, unchanged); +} + +#[test] +fn secp256k1_recover_and_verify() { + let mut pubkey = zkvm_secp256k1_pubkey { data: [0; 64] }; + let status = unsafe { zkvm_secp256k1_ecrecover(&MSG, &SIG, 1, &mut pubkey) }; + assert_eq!(status, ZKVM_EOK); + + let mut verified = false; + let status = unsafe { zkvm_secp256k1_verify(&MSG, &SIG, &pubkey, &mut verified) }; + assert_eq!(status, ZKVM_EOK); + assert!(verified); + + let mut wrong_msg = MSG; + wrong_msg.data[0] ^= 1; + let status = unsafe { zkvm_secp256k1_verify(&wrong_msg, &SIG, &pubkey, &mut verified) }; + assert_eq!(status, ZKVM_EOK); + assert!(!verified); + + let bad_pubkey = zkvm_secp256k1_pubkey { data: [0xff; 64] }; + let status = unsafe { zkvm_secp256k1_verify(&MSG, &SIG, &bad_pubkey, &mut verified) }; + assert_eq!(status, ZKVM_EOK); + assert!(!verified); +} + +#[test] +fn secp256k1_null_pointers() { + let mut pubkey = zkvm_secp256k1_pubkey { data: [0; 64] }; + assert_eq!( + unsafe { zkvm_secp256k1_ecrecover(core::ptr::null(), &SIG, 1, &mut pubkey) }, + ZKVM_EFAIL + ); + assert_eq!( + unsafe { zkvm_secp256k1_ecrecover(&MSG, core::ptr::null(), 1, &mut pubkey) }, + ZKVM_EFAIL + ); + assert_eq!( + unsafe { zkvm_secp256k1_ecrecover(&MSG, &SIG, 1, core::ptr::null_mut()) }, + ZKVM_EFAIL + ); + + let mut verified = false; + assert_eq!( + unsafe { zkvm_secp256k1_verify(core::ptr::null(), &SIG, &pubkey, &mut verified) }, + ZKVM_EFAIL + ); + assert_eq!( + unsafe { zkvm_secp256k1_verify(&MSG, core::ptr::null(), &pubkey, &mut verified) }, + ZKVM_EFAIL + ); + assert_eq!( + unsafe { zkvm_secp256k1_verify(&MSG, &SIG, core::ptr::null(), &mut verified) }, + ZKVM_EFAIL + ); + assert_eq!( + unsafe { zkvm_secp256k1_verify(&MSG, &SIG, &pubkey, core::ptr::null_mut()) }, + ZKVM_EFAIL + ); +} diff --git a/crates/accelerators/tests/secp256r1.rs b/crates/accelerators/tests/secp256r1.rs new file mode 100644 index 000000000..f8356b7bf --- /dev/null +++ b/crates/accelerators/tests/secp256r1.rs @@ -0,0 +1,81 @@ +//! secp256r1 C-interface conformance vectors. +//! +//! Vectors are from https://github.com/daimo-eth/p256-verifier/tree/master/test-vectors. + +use hex_literal::hex; +use openvm_accelerators::{ + zkvm_secp256r1_hash, zkvm_secp256r1_pubkey, zkvm_secp256r1_signature, zkvm_secp256r1_verify, + ZKVM_EFAIL, ZKVM_EOK, +}; + +fn parts( + input: &[u8; 160], +) -> (zkvm_secp256r1_hash, zkvm_secp256r1_signature, zkvm_secp256r1_pubkey) { + ( + zkvm_secp256r1_hash { data: input[..32].try_into().unwrap() }, + zkvm_secp256r1_signature { data: input[32..96].try_into().unwrap() }, + zkvm_secp256r1_pubkey { data: input[96..].try_into().unwrap() }, + ) +} + +const VALID: [u8; 160] = hex!( + "4cee90eb86eaa050036147a12d49004b6b9c72bd725d39d4785011fe190f0b4d" + "a73bd4903f0ce3b639bbbf6e8e80d16931ff4bcf5993d58468e8fb19086e8cac" + "36dbcd03009df8c59286b162af3bd7fcc0450c9aa81be5d10d312af6c66b1d60" + "4aebd3099c618202fcfe16ae7770b0c49ab5eadf74b754204a3bb6060e44eff3" + "7618b065f9832de4ca6ca971a7a1adc826d0f7c00181a5fb2ddf79ae00b4e10e" +); + +#[test] +fn secp256r1_verify_vectors() { + let (msg, sig, pubkey) = parts(&VALID); + let mut verified = false; + let status = unsafe { zkvm_secp256r1_verify(&msg, &sig, &pubkey, &mut verified) }; + assert_eq!(status, ZKVM_EOK); + assert!(verified); + + let mut wrong_msg = msg; + wrong_msg.data[0] = 0x3c; + let status = unsafe { zkvm_secp256r1_verify(&wrong_msg, &sig, &pubkey, &mut verified) }; + assert_eq!(status, ZKVM_EOK); + assert!(!verified); +} + +#[test] +fn secp256r1_verify_malformed_inputs() { + let (msg, sig, pubkey) = parts(&VALID); + let mut verified = true; + + let bad_sig = zkvm_secp256r1_signature { data: [0xff; 64] }; + let status = unsafe { zkvm_secp256r1_verify(&msg, &bad_sig, &pubkey, &mut verified) }; + assert_eq!(status, ZKVM_EOK); + assert!(!verified); + + let bad_pubkey = zkvm_secp256r1_pubkey { data: [0; 64] }; + let status = unsafe { zkvm_secp256r1_verify(&msg, &sig, &bad_pubkey, &mut verified) }; + assert_eq!(status, ZKVM_EOK); + assert!(!verified); +} + +#[test] +fn secp256r1_verify_null_pointers() { + let (msg, sig, pubkey) = parts(&VALID); + let mut verified = false; + + assert_eq!( + unsafe { zkvm_secp256r1_verify(core::ptr::null(), &sig, &pubkey, &mut verified) }, + ZKVM_EFAIL + ); + assert_eq!( + unsafe { zkvm_secp256r1_verify(&msg, core::ptr::null(), &pubkey, &mut verified) }, + ZKVM_EFAIL + ); + assert_eq!( + unsafe { zkvm_secp256r1_verify(&msg, &sig, core::ptr::null(), &mut verified) }, + ZKVM_EFAIL + ); + assert_eq!( + unsafe { zkvm_secp256r1_verify(&msg, &sig, &pubkey, core::ptr::null_mut()) }, + ZKVM_EFAIL + ); +} diff --git a/crates/accelerators/tests/sha256.rs b/crates/accelerators/tests/sha256.rs new file mode 100644 index 000000000..a4bf78cfc --- /dev/null +++ b/crates/accelerators/tests/sha256.rs @@ -0,0 +1,36 @@ +//! SHA-256 C-interface conformance vectors. + +use hex_literal::hex; +use openvm_accelerators::{zkvm_sha256, zkvm_sha256_hash, ZKVM_EFAIL, ZKVM_EOK}; + +#[test] +fn sha256_abc() { + let data = *b"abc"; + let mut output = zkvm_sha256_hash { data: [0; 32] }; + let status = unsafe { zkvm_sha256(data.as_ptr(), data.len(), &mut output) }; + assert_eq!(status, ZKVM_EOK); + assert_eq!( + output.data, + hex!("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad") + ); +} + +#[test] +fn sha256_null_pointers() { + let data = *b"abc"; + let mut output = zkvm_sha256_hash { data: [0; 32] }; + + // A NULL `data` with `len == 0` is the empty input. + let status = unsafe { zkvm_sha256(core::ptr::null(), 0, &mut output) }; + assert_eq!(status, ZKVM_EOK); + assert_eq!( + output.data, + hex!("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") + ); + + let status = unsafe { zkvm_sha256(core::ptr::null(), data.len(), &mut output) }; + assert_eq!(status, ZKVM_EFAIL); + + let status = unsafe { zkvm_sha256(data.as_ptr(), data.len(), core::ptr::null_mut()) }; + assert_eq!(status, ZKVM_EFAIL); +} diff --git a/crates/revm-crypto/Cargo.toml b/crates/revm-crypto/Cargo.toml index 226cc5ef1..fea11ff40 100644 --- a/crates/revm-crypto/Cargo.toml +++ b/crates/revm-crypto/Cargo.toml @@ -12,33 +12,19 @@ workspace = true [dependencies] # revm revm.workspace = true +openvm-accelerators.workspace = true # alloy alloy-primitives = { workspace = true, features = ["map-foldhash"] } alloy-consensus = { workspace = true, features = ["crypto-backend"] } -# OpenVM dependencies for optimized crypto (only needed when running in zkvm) -openvm-curve-utils = { workspace = true, features = ["bn254", "bls12_381"] } -openvm-ecc-guest = { workspace = true } -openvm-sha2 = { workspace = true } -openvm-pairing = { workspace = true, features = ["bn254", "bls12_381"] } -openvm-k256 = { workspace = true } -openvm-p256 = { workspace = true } -openvm-kzg = { workspace = true, features = ["use-intrinsics"] } -openvm-keccak256 = { workspace = true } -aurora-engine-modexp = { version = "1.2.0", default-features = false } - [target.'cfg(all(target_arch = "riscv64", any(target_os = "none", target_os = "openvm")))'.dependencies] revm-primitives = { workspace = true, features = ["hashbrown"] } alloy-primitives = { workspace = true, features = ["native-keccak"] } -[target.'cfg(not(any(target_os = "none", target_os = "openvm")))'.dependencies] -openvm-sha2 = { workspace = true, features = ["import_sha2"] } -openvm-keccak256 = { workspace = true, features = ["tiny_keccak"] } - [features] default = [] -std = [] +std = ["openvm-accelerators/std"] [package.metadata.cargo-shear] ignored = ["revm-primitives"] diff --git a/crates/revm-crypto/src/alloy.rs b/crates/revm-crypto/src/alloy.rs new file mode 100644 index 000000000..2c0980d24 --- /dev/null +++ b/crates/revm-crypto/src/alloy.rs @@ -0,0 +1,67 @@ +//! Alloy signer adapter for the standard zkVM accelerator C interface. + +use alloc::{boxed::Box, sync::Arc}; +use core::error::Error; + +use crate::status_ok; +use alloy_consensus::crypto::{ + backend::{install_default_provider, CryptoProvider}, + RecoveryError, +}; +use alloy_primitives::Address; +use openvm_accelerators::{ + zkvm_keccak256, zkvm_keccak256_hash, zkvm_secp256k1_ecrecover, zkvm_secp256k1_hash, + zkvm_secp256k1_pubkey, zkvm_secp256k1_signature, zkvm_secp256k1_verify, +}; + +#[derive(Debug, Default)] +struct OpenVmK256Provider; + +impl CryptoProvider for OpenVmK256Provider { + fn recover_signer_unchecked( + &self, + signature: &[u8; 65], + msg: &[u8; 32], + ) -> Result { + let msg = zkvm_secp256k1_hash { data: *msg }; + let sig = zkvm_secp256k1_signature { data: signature[..64].try_into().unwrap() }; + let mut pubkey = zkvm_secp256k1_pubkey { data: [0; 64] }; + let status = unsafe { zkvm_secp256k1_ecrecover(&msg, &sig, signature[64], &mut pubkey) }; + if !status_ok(status) { + return Err(RecoveryError::new()); + } + Ok(address_from_pubkey(&pubkey.data)) + } + + fn verify_and_compute_signer_unchecked( + &self, + pubkey: &[u8; 65], + sig: &[u8; 64], + msg: &[u8; 32], + ) -> Result { + if pubkey[0] != 0x04 { + return Err(RecoveryError::new()); + } + let msg = zkvm_secp256k1_hash { data: *msg }; + let sig = zkvm_secp256k1_signature { data: *sig }; + let pubkey = zkvm_secp256k1_pubkey { data: pubkey[1..].try_into().unwrap() }; + let mut verified = false; + let status = unsafe { zkvm_secp256k1_verify(&msg, &sig, &pubkey, &mut verified) }; + if !status_ok(status) || !verified { + return Err(RecoveryError::new()); + } + Ok(address_from_pubkey(&pubkey.data)) + } +} + +fn address_from_pubkey(pubkey: &[u8; 64]) -> Address { + let mut hash = zkvm_keccak256_hash { data: [0; 32] }; + let status = unsafe { zkvm_keccak256(pubkey.as_ptr(), pubkey.len(), &mut hash) }; + assert!(status_ok(status), "zkVM accelerator call failed"); + Address::from_slice(&hash.data[12..]) +} + +pub(super) fn install() -> Result<(), Box> { + install_default_provider(Arc::new(OpenVmK256Provider))?; + Ok(()) +} diff --git a/crates/revm-crypto/src/lib.rs b/crates/revm-crypto/src/lib.rs index 090e3e623..c3fe83c15 100644 --- a/crates/revm-crypto/src/lib.rs +++ b/crates/revm-crypto/src/lib.rs @@ -1,733 +1,24 @@ -//! OpenVM Crypto Implementation for REVM +//! OpenVM crypto providers for REVM and Alloy. //! -//! This module provides OpenVM-optimized implementations of cryptographic operations -//! for both transaction validation (via Alloy crypto provider) and precompile execution. +//! These adapters use the standard zkVM accelerator C interface. #![cfg_attr(not(feature = "std"), no_std)] extern crate alloc; -use alloc::{boxed::Box, sync::Arc, vec, vec::Vec}; -use alloy_consensus::crypto::{ - backend::{install_default_provider, CryptoProvider}, - RecoveryError, -}; -use alloy_primitives::Address; -use openvm_ecc_guest::{ - algebra::IntMod, - weierstrass::{IntrinsicCurve, WeierstrassPoint}, - AffinePoint, Group, -}; -use openvm_k256::ecdsa::{signature::hazmat::PrehashVerifier, RecoveryId, Signature, VerifyingKey}; -use openvm_keccak256::keccak256; -use openvm_kzg::{Bytes32, Bytes48, KzgProof}; -use openvm_pairing::{ - bls12_381::{self as bls, Bls12_381}, - bn254::{self as bn, Bn254}, - PairingCheck, -}; -use revm::{ - install_crypto, - precompile::{ - bls12_381::{ - G1Point as BlsG1Point, G1PointScalar as BlsG1PointScalar, G2Point as BlsG2Point, - G2PointScalar as BlsG2PointScalar, - }, - bls12_381_const::{ - FP_LENGTH as BLS_FP_LEN, G1_LENGTH as BLS_G1_LEN, G2_LENGTH as BLS_G2_LEN, - SCALAR_LENGTH as BLS_SCALAR_LEN, - }, - Crypto, PrecompileHalt, - }, -}; +mod alloy; +mod revm; -use openvm_curve_utils::SubgroupCheck; +use alloc::boxed::Box; +use core::error::Error; +use openvm_accelerators::{zkvm_status, ZKVM_EOK}; -// BN254 constants -const BN_FQ_LEN: usize = 32; -const BN_G1_LEN: usize = 64; -const BN_G2_LEN: usize = 128; -/// BN_SCALAR_LEN specifies the number of bytes needed to represent an Fr element. -/// This is an element in the scalar field of BN254. -const BN_SCALAR_LEN: usize = 32; - -/// OpenVM k256 backend for Alloy crypto operations (transaction validation) -#[derive(Debug, Default)] -struct OpenVmK256Provider; - -impl CryptoProvider for OpenVmK256Provider { - fn recover_signer_unchecked( - &self, - sig: &[u8; 65], - msg: &[u8; 32], - ) -> Result { - // Extract components: sig[0..32]=r, sig[32..64]=s, sig[64]=recovery_id - // Parse signature using OpenVM k256 - let mut signature = Signature::from_slice(&sig[..64]).map_err(|_| RecoveryError::new())?; - - // Normalize signature if needed - let mut recid = sig[64]; - if let Some(sig_normalized) = signature.normalize_s() { - signature = sig_normalized; - recid ^= 1; - } - - // Create recovery ID - let recovery_id = RecoveryId::from_byte(recid).ok_or(RecoveryError::new())?; - - // Recover public key using OpenVM - let recovered_key = - VerifyingKey::recover_from_prehash_noverify(msg, &signature.to_bytes(), recovery_id) - .map_err(|_| RecoveryError::new())?; - - // Hash the uncompressed SEC1 key without the 0x04 prefix. - let public_key = recovered_key.to_encoded_point(false); - let encoded_pubkey = &public_key.as_bytes()[1..65]; - - // Hash to get Ethereum address - let pubkey_hash = keccak256(encoded_pubkey); - let address_bytes = &pubkey_hash[12..32]; // Last 20 bytes - - Ok(Address::from_slice(address_bytes)) - } - - fn verify_and_compute_signer_unchecked( - &self, - pubkey: &[u8; 65], - sig: &[u8; 64], - msg: &[u8; 32], - ) -> Result { - let vk = VerifyingKey::from_sec1_bytes(pubkey).map_err(|_| RecoveryError::new())?; - - let mut signature = Signature::from_slice(sig).map_err(|_| RecoveryError::new())?; - if let Some(sig_normalized) = signature.normalize_s() { - signature = sig_normalized; - } - - vk.verify_prehash(msg.as_ref(), &signature).map_err(|_| RecoveryError::new())?; - - // Compute address directly from the provided pubkey bytes (skip 0x04 prefix) - let pubkey_hash = keccak256(&pubkey[1..65]); - Ok(Address::from_slice(&pubkey_hash[12..32])) - } -} - -/// OpenVM custom crypto implementation for faster precompiles -#[derive(Debug, Default)] -struct OpenVmCrypto; - -impl Crypto for OpenVmCrypto { - /// Custom SHA-256 implementation with openvm optimization - fn sha256(&self, input: &[u8]) -> [u8; 32] { - #[cfg(not(openvm_intrinsics))] - use openvm_sha2::Digest; - openvm_sha2::Sha256::digest(input).into() - } - - /// Custom BN254 G1 addition with openvm optimization - fn bn254_g1_add(&self, p1_bytes: &[u8], p2_bytes: &[u8]) -> Result<[u8; 64], PrecompileHalt> { - let p1 = read_bn_g1_point(p1_bytes)?; - let p2 = read_bn_g1_point(p2_bytes)?; - let result = p1 + p2; - Ok(encode_bn_g1_point(result)) - } - - /// Custom BN254 G1 scalar multiplication with openvm optimization - fn bn254_g1_mul( - &self, - point_bytes: &[u8], - scalar_bytes: &[u8], - ) -> Result<[u8; 64], PrecompileHalt> { - let p = read_bn_g1_point(point_bytes)?; - let s = read_bn_scalar(scalar_bytes); - let result = Bn254::msm(&[s], &[p]); - Ok(encode_bn_g1_point(result)) - } - - /// Custom BN254 pairing check with openvm optimization - fn bn254_pairing_check(&self, pairs: &[(&[u8], &[u8])]) -> Result { - if pairs.is_empty() { - return Ok(true); - } - let mut g1_points = Vec::with_capacity(pairs.len()); - let mut g2_points = Vec::with_capacity(pairs.len()); - - for (g1_bytes, g2_bytes) in pairs { - let g1 = read_bn_g1_point(g1_bytes)?; - let g2 = read_bn_g2_point(g2_bytes)?; - - let (g1_x, g1_y) = g1.into_coords(); - let g1 = AffinePoint::new(g1_x, g1_y); - - let (g2_x, g2_y) = g2.into_coords(); - let g2 = AffinePoint::new(g2_x, g2_y); - - g1_points.push(g1); - g2_points.push(g2); - } - - let pairing_result = Bn254::pairing_check(&g1_points, &g2_points).is_ok(); - Ok(pairing_result) - } - - /// Custom BLS12-381 G1 addition with openvm optimization - fn bls12_381_g1_add( - &self, - a: BlsG1Point, - b: BlsG1Point, - ) -> Result<[u8; BLS_G1_LEN], PrecompileHalt> { - // EIP-2537 G1ADD validates on-curve only, not subgroup membership. - let p1 = read_bls_g1_point_no_subgroup_check(&a)?; - let p2 = read_bls_g1_point_no_subgroup_check(&b)?; - let sum = p1 + p2; - Ok(encode_bls_g1_point(&sum)) - } - - /// Custom BLS12-381 G1 MSM with openvm optimization - fn bls12_381_g1_msm( - &self, - pairs: &mut dyn Iterator>, - ) -> Result<[u8; BLS_G1_LEN], PrecompileHalt> { - let mut scalars = Vec::new(); - let mut points = Vec::new(); - - for pair in pairs { - let (point_bytes, scalar_bytes) = pair?; - points.push(read_bls_g1_point(&point_bytes)?); - scalars.push(read_bls_scalar(&scalar_bytes)); - } - - if points.is_empty() { - return Ok([0u8; BLS_G1_LEN]); - } - - let result = Bls12_381::msm(&scalars, &points); - Ok(encode_bls_g1_point(&result)) - } - - /// Custom BLS12-381 G2 addition with openvm optimization - fn bls12_381_g2_add( - &self, - a: BlsG2Point, - b: BlsG2Point, - ) -> Result<[u8; BLS_G2_LEN], PrecompileHalt> { - // EIP-2537 G2ADD validates on-curve only, not subgroup membership. - let p1 = read_bls_g2_point_no_subgroup_check(&a)?; - let p2 = read_bls_g2_point_no_subgroup_check(&b)?; - let sum = p1 + p2; - Ok(encode_bls_g2_point(&sum)) - } - - /// Custom BLS12-381 G2 MSM with openvm optimization - fn bls12_381_g2_msm( - &self, - pairs: &mut dyn Iterator>, - ) -> Result<[u8; BLS_G2_LEN], PrecompileHalt> { - let mut scalars = Vec::new(); - let mut points = Vec::new(); - - for pair in pairs { - let (point_bytes, scalar_bytes) = pair?; - points.push(read_bls_g2_point(&point_bytes)?); - scalars.push(read_bls_scalar(&scalar_bytes)); - } - - if points.is_empty() { - return Ok([0u8; BLS_G2_LEN]); - } - - // directly using openvm_ecc_guest::msm here - let result = openvm_ecc_guest::msm(&scalars, &points); - Ok(encode_bls_g2_point(&result)) - } - - /// Custom BLS12-381 pairing check with openvm optimization - fn bls12_381_pairing_check( - &self, - pairs: &[(BlsG1Point, BlsG2Point)], - ) -> Result { - if pairs.is_empty() { - return Ok(true); - } - - let mut g1_points = Vec::with_capacity(pairs.len()); - let mut g2_points = Vec::with_capacity(pairs.len()); - - for (g1_bytes, g2_bytes) in pairs { - let g1 = read_bls_g1_point(g1_bytes)?; - let g2 = read_bls_g2_point(g2_bytes)?; - - let (g1_x, g1_y) = g1.into_coords(); - let (g2_x, g2_y) = g2.into_coords(); - - g1_points.push(AffinePoint::new(g1_x, g1_y)); - g2_points.push(AffinePoint::new(g2_x, g2_y)); - } - - let pairing_result = Bls12_381::pairing_check(&g1_points, &g2_points).is_ok(); - Ok(pairing_result) - } - - /// Custom secp256k1 ECDSA signature recovery with openvm optimization - fn secp256k1_ecrecover( - &self, - sig_bytes: &[u8; 64], - mut recid: u8, - msg_hash: &[u8; 32], - ) -> Result<[u8; 32], PrecompileHalt> { - let mut sig = Signature::from_slice(sig_bytes) - .map_err(|_| PrecompileHalt::other("Invalid signature format"))?; - - if let Some(sig_normalized) = sig.normalize_s() { - sig = sig_normalized; - recid ^= 1; - } - - let recovery_id = RecoveryId::from_byte(recid) - .ok_or_else(|| PrecompileHalt::other("Invalid recovery ID"))?; - - let recovered_key = - VerifyingKey::recover_from_prehash_noverify(msg_hash, &sig.to_bytes(), recovery_id) - .map_err(|_| PrecompileHalt::other("Key recovery failed"))?; - - let public_key = recovered_key.to_encoded_point(false); - let encoded_pubkey = &public_key.as_bytes()[1..65]; - - let pubkey_hash = keccak256(encoded_pubkey); - let mut address = [0u8; 32]; - address[12..].copy_from_slice(&pubkey_hash[12..]); - - Ok(address) - } - - /// Custom secp256r1 signature verification with openvm optimization - fn secp256r1_verify_signature(&self, msg: &[u8; 32], sig: &[u8; 64], pk: &[u8; 64]) -> bool { - use openvm_p256::{ - ecdsa::{signature::hazmat::PrehashVerifier, Signature, VerifyingKey}, - EncodedPoint, - }; - - // Can fail only if the input is not exact length. - let Ok(signature) = Signature::from_slice(sig) else { - return false; - }; - // Decode the public key bytes (x,y coordinates) using EncodedPoint - let encoded_point = EncodedPoint::from_untagged_bytes(&(*pk).into()); - // Create VerifyingKey from the encoded point - let Ok(public_key) = VerifyingKey::from_encoded_point(&encoded_point) else { - return false; - }; - - public_key.verify_prehash(msg, &signature).is_ok() - } - - /// Custom KZG point evaluation with configurable backends - fn verify_kzg_proof( - &self, - z: &[u8; 32], - y: &[u8; 32], - commitment: &[u8; 48], - proof: &[u8; 48], - ) -> Result<(), PrecompileHalt> { - let env = openvm_kzg::EnvKzgSettings::default(); - let kzg_settings = env.get(); - - let commitment_bytes = Bytes48::from_slice(commitment) - .map_err(|_| PrecompileHalt::other("invalid commitment bytes"))?; - let z_bytes = - Bytes32::from_slice(z).map_err(|_| PrecompileHalt::other("invalid z bytes"))?; - let y_bytes = - Bytes32::from_slice(y).map_err(|_| PrecompileHalt::other("invalid y bytes"))?; - let proof_bytes = - Bytes48::from_slice(proof).map_err(|_| PrecompileHalt::other("invalid proof bytes"))?; - - let valid = KzgProof::verify_kzg_proof( - &commitment_bytes, - &z_bytes, - &y_bytes, - &proof_bytes, - kzg_settings, - ) - .map_err(|_| PrecompileHalt::other("openvm kzg proof verification failed"))?; - if valid { - Ok(()) - } else { - Err(PrecompileHalt::BlobVerifyKzgProofFailed) - } - } - - /// Custom modular exponentiation with BN254 Fr acceleration - fn modexp(&self, base: &[u8], exp: &[u8], modulus: &[u8]) -> Result, PrecompileHalt> { - if is_bn254_fr(modulus) { - return Ok(accelerated_modexp_bn254_fr(base, exp)); - } - Ok(aurora_engine_modexp::modexp(base, exp, modulus)) - } -} - -/// Returns true if the modulus (big-endian, possibly with leading zeros) equals BN254 Fr. -fn is_bn254_fr(modulus: &[u8]) -> bool { - // Strip leading zeros - let stripped = match modulus.iter().position(|&b| b != 0) { - Some(i) => &modulus[i..], - None => return false, // all zeros - }; - // bn::Scalar::MODULUS is little-endian; compare against reversed input - stripped.len() == BN_SCALAR_LEN && stripped.iter().rev().eq(bn::Scalar::MODULUS.as_ref().iter()) +fn status_ok(status: zkvm_status) -> bool { + status == ZKVM_EOK } -/// Accelerated modexp for BN254 Fr using field arithmetic intrinsics. -fn accelerated_modexp_bn254_fr(base: &[u8], exp: &[u8]) -> Vec { - use openvm_ecc_guest::algebra::{ExpBytes, Reduce}; - - // OpenVM's field reduction requires inputs to be aligned to the field byte size. - let padded_len = base.len().next_multiple_of(BN_SCALAR_LEN).max(BN_SCALAR_LEN); - let mut padded = vec![0u8; padded_len]; - padded[padded_len - base.len()..].copy_from_slice(base); - let base_fr = bn::Scalar::reduce_be_bytes(&padded); - - base_fr.exp_bytes(true, exp).to_be_bytes().as_ref().to_vec() -} - -/// Install OpenVM crypto implementations globally -pub fn install_openvm_crypto() -> Result> { - // Install OpenVM k256 provider for Alloy (transaction validation) - install_default_provider(Arc::new(OpenVmK256Provider))?; - - // Install OpenVM crypto for REVM precompiles - let installed = install_crypto(OpenVmCrypto); - - Ok(installed) -} - -// Helper functions for BN254 operations - -#[inline] -fn read_bn_fq(input: &[u8]) -> Result { - if input.len() < BN_FQ_LEN { - Err(PrecompileHalt::Bn254FieldPointNotAMember) - } else { - bn::Fp::from_be_bytes(&input[..BN_FQ_LEN]).ok_or(PrecompileHalt::Bn254FieldPointNotAMember) - } -} - -#[inline] -fn read_bn_fq2(input: &[u8]) -> Result { - let y = read_bn_fq(&input[..BN_FQ_LEN])?; - let x = read_bn_fq(&input[BN_FQ_LEN..BN_FQ_LEN * 2])?; - Ok(bn::Fp2::new(x, y)) -} - -#[inline] -fn read_bn_g1_point(input: &[u8]) -> Result { - if input.len() != BN_G1_LEN { - return Err(PrecompileHalt::Bn254PairLength); - } - let px = read_bn_fq(&input[0..BN_FQ_LEN])?; - let py = read_bn_fq(&input[BN_FQ_LEN..BN_G1_LEN])?; - // SAFETY: `read_bn_fq` produces canonical Fp elements; `from_xy` itself checks the curve - // equation and returns `None` if `(px, py)` is not on the curve. - let point = unsafe { bn::G1Affine::from_xy(px, py) } - .ok_or(PrecompileHalt::Bn254AffineGFailedToCreate)?; - if point.is_in_correct_subgroup() { - Ok(point) - } else { - Err(PrecompileHalt::Bn254AffineGFailedToCreate) - } -} - -#[inline] -fn read_bn_g2_point(input: &[u8]) -> Result { - if input.len() != BN_G2_LEN { - return Err(PrecompileHalt::Bn254PairLength); - } - let c0 = read_bn_fq2(&input[0..BN_G1_LEN])?; - let c1 = read_bn_fq2(&input[BN_G1_LEN..BN_G2_LEN])?; - // SAFETY: `read_bn_fq2` produces canonical Fp2 elements; `from_xy` itself checks the curve - // equation and returns `None` if `(c0, c1)` is not on the twist. - let point = unsafe { bn::G2Affine::from_xy(c0, c1) } - .ok_or(PrecompileHalt::Bn254AffineGFailedToCreate)?; - if point.is_in_correct_subgroup() { - Ok(point) - } else { - Err(PrecompileHalt::Bn254AffineGFailedToCreate) - } -} - -#[inline] -fn encode_bn_g1_point(point: bn::G1Affine) -> [u8; BN_G1_LEN] { - let mut output = [0u8; BN_G1_LEN]; - - let x_bytes: &[u8] = point.x().as_le_bytes(); - let y_bytes: &[u8] = point.y().as_le_bytes(); - for i in 0..BN_FQ_LEN { - output[i] = x_bytes[BN_FQ_LEN - 1 - i]; - output[i + BN_FQ_LEN] = y_bytes[BN_FQ_LEN - 1 - i]; - } - output -} - -/// Reads a scalar from the input slice -/// -/// Note: The scalar does not need to be canonical. -/// -/// # Panics -/// -/// If `input.len()` is not equal to [`BN_SCALAR_LEN`]. -#[inline] -fn read_bn_scalar(input: &[u8]) -> bn::Scalar { - assert_eq!( - input.len(), - BN_SCALAR_LEN, - "unexpected scalar length. got {}, expected {BN_SCALAR_LEN}", - input.len() - ); - bn::Scalar::from_be_bytes_unchecked(input) -} - -// Helper functions for BLS12-381 operations - -#[inline] -fn read_bls_fp(input: &[u8]) -> Result { - if input.len() != BLS_FP_LEN { - return Err(PrecompileHalt::other("invalid BLS12-381 fp length")); - } - bls::Fp::from_be_bytes(input) - .ok_or_else(|| PrecompileHalt::other("element not in BLS12-381 base field")) -} - -#[inline] -fn read_bls_fp2(c0: &[u8], c1: &[u8]) -> Result { - let real = read_bls_fp(c0)?; - let imag = read_bls_fp(c1)?; - Ok(bls::Fp2::new(real, imag)) -} - -#[inline] -fn read_bls_g1_point_no_subgroup_check( - point: &BlsG1Point, -) -> Result { - let px = read_bls_fp(&point.0)?; - let py = read_bls_fp(&point.1)?; - // SAFETY: `read_bls_fp` produces canonical Fp elements; `from_xy` itself checks the curve - // equation and returns `None` if `(px, py)` is not on the curve. - unsafe { bls::G1Affine::from_xy(px, py) }.ok_or(PrecompileHalt::Bls12381G1NotOnCurve) -} - -#[inline] -fn read_bls_g1_point(point: &BlsG1Point) -> Result { - let point = read_bls_g1_point_no_subgroup_check(point)?; - if point.is_in_correct_subgroup() { - Ok(point) - } else { - Err(PrecompileHalt::Bls12381G1NotInSubgroup) - } -} - -#[inline] -fn read_bls_g2_point_no_subgroup_check( - point: &BlsG2Point, -) -> Result { - let x = read_bls_fp2(&point.0, &point.1)?; - let y = read_bls_fp2(&point.2, &point.3)?; - // SAFETY: `read_bls_fp2` produces canonical Fp2 elements; `from_xy` itself checks the curve - // equation and returns `None` if `(x, y)` is not on the twist. - unsafe { bls::G2Affine::from_xy(x, y) }.ok_or(PrecompileHalt::Bls12381G2NotOnCurve) -} - -#[inline] -fn read_bls_g2_point(point: &BlsG2Point) -> Result { - let point = read_bls_g2_point_no_subgroup_check(point)?; - if point.is_in_correct_subgroup() { - Ok(point) - } else { - Err(PrecompileHalt::Bls12381G2NotInSubgroup) - } -} - -#[inline] -fn read_bls_scalar(input: &[u8]) -> bls::Scalar { - assert_eq!( - input.len(), - BLS_SCALAR_LEN, - "unexpected scalar length. got {}, expected {BLS_SCALAR_LEN}", - input.len() - ); - bls::Scalar::from_be_bytes_unchecked(input) -} - -#[inline] -fn encode_bls_g1_point(point: &bls::G1Affine) -> [u8; BLS_G1_LEN] { - if point.is_identity() { - return [0u8; BLS_G1_LEN]; - } - - let mut output = [0u8; BLS_G1_LEN]; - let x_bytes: &[u8] = point.x().as_le_bytes(); - let y_bytes: &[u8] = point.y().as_le_bytes(); - for i in 0..BLS_FP_LEN { - output[i] = x_bytes[BLS_FP_LEN - 1 - i]; - output[i + BLS_FP_LEN] = y_bytes[BLS_FP_LEN - 1 - i]; - } - output -} - -#[inline] -fn encode_bls_g2_point(point: &bls::G2Affine) -> [u8; BLS_G2_LEN] { - if point.is_identity() { - return [0u8; BLS_G2_LEN]; - } - - let mut output = [0u8; BLS_G2_LEN]; - let x = point.x(); - let y = point.y(); - let x_c0 = x.c0.as_le_bytes(); - let x_c1 = x.c1.as_le_bytes(); - let y_c0 = y.c0.as_le_bytes(); - let y_c1 = y.c1.as_le_bytes(); - for i in 0..BLS_FP_LEN { - output[i] = x_c0[BLS_FP_LEN - 1 - i]; - output[i + BLS_FP_LEN] = x_c1[BLS_FP_LEN - 1 - i]; - output[i + (2 * BLS_FP_LEN)] = y_c0[BLS_FP_LEN - 1 - i]; - output[i + (3 * BLS_FP_LEN)] = y_c1[BLS_FP_LEN - 1 - i]; - } - output -} - -#[cfg(test)] -mod tests { - use super::*; - - /// Runs `secp256r1_verify_signature` on a 160-byte P256VERIFY input (msg || sig || pk). - fn p256_verify_input(input_hex: &str) -> bool { - let input = alloy_primitives::hex::decode(input_hex).unwrap(); - assert_eq!(input.len(), 160); - OpenVmCrypto.secp256r1_verify_signature( - input[..32].try_into().unwrap(), - input[32..96].try_into().unwrap(), - input[96..160].try_into().unwrap(), - ) - } - - // Test vectors from https://github.com/daimo-eth/p256-verifier/tree/master/test-vectors, - // as used by revm-precompile's secp256r1 tests. - #[test] - fn test_secp256r1_verify_signature() { - // valid signature - assert!(p256_verify_input("4cee90eb86eaa050036147a12d49004b6b9c72bd725d39d4785011fe190f0b4da73bd4903f0ce3b639bbbf6e8e80d16931ff4bcf5993d58468e8fb19086e8cac36dbcd03009df8c59286b162af3bd7fcc0450c9aa81be5d10d312af6c66b1d604aebd3099c618202fcfe16ae7770b0c49ab5eadf74b754204a3bb6060e44eff37618b065f9832de4ca6ca971a7a1adc826d0f7c00181a5fb2ddf79ae00b4e10e")); - assert!(p256_verify_input("3fec5769b5cf4e310a7d150508e82fb8e3eda1c2c94c61492d3bd8aea99e06c9e22466e928fdccef0de49e3503d2657d00494a00e764fd437bdafa05f5922b1fbbb77c6817ccf50748419477e843d5bac67e6a70e97dde5a57e0c983b777e1ad31a80482dadf89de6302b1988c82c29544c9c07bb910596158f6062517eb089a2f54c9a0f348752950094d3228d3b940258c75fe2a413cb70baa21dc2e352fc5")); - // wrong message - assert!(!p256_verify_input("3cee90eb86eaa050036147a12d49004b6b9c72bd725d39d4785011fe190f0b4da73bd4903f0ce3b639bbbf6e8e80d16931ff4bcf5993d58468e8fb19086e8cac36dbcd03009df8c59286b162af3bd7fcc0450c9aa81be5d10d312af6c66b1d604aebd3099c618202fcfe16ae7770b0c49ab5eadf74b754204a3bb6060e44eff37618b065f9832de4ca6ca971a7a1adc826d0f7c00181a5fb2ddf79ae00b4e10e")); - // signature values out of range - assert!(!p256_verify_input("4cee90eb86eaa050036147a12d49004b6b9c72bd725d39d4785011fe190f0b4dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff4aebd3099c618202fcfe16ae7770b0c49ab5eadf74b754204a3bb6060e44eff37618b065f9832de4ca6ca971a7a1adc826d0f7c00181a5fb2ddf79ae00b4e10e")); - // public key not on the curve - assert!(!p256_verify_input("4cee90eb86eaa050036147a12d49004b6b9c72bd725d39d4785011fe190f0b4da73bd4903f0ce3b639bbbf6e8e80d16931ff4bcf5993d58468e8fb19086e8cac36dbcd03009df8c59286b162af3bd7fcc0450c9aa81be5d10d312af6c66b1d6000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")); - } - - /// BN254 Fr modulus in big-endian bytes - fn bn254_fr_modulus_be() -> Vec { - let m = bn::Scalar::MODULUS; - m.as_ref().iter().rev().copied().collect() - } - - /// Reference implementation: aurora_engine_modexp - fn reference_modexp(base: &[u8], exp: &[u8], modulus: &[u8]) -> Vec { - aurora_engine_modexp::modexp(base, exp, modulus) - } - - /// Helper: run accelerated and compare against reference. - /// The accelerated path always returns BN_SCALAR_LEN bytes, so we left-pad the - /// reference output to match. - fn check(base: &[u8], exp: &[u8]) { - let modulus = bn254_fr_modulus_be(); - let expected = reference_modexp(base, exp, &modulus); - let actual = accelerated_modexp_bn254_fr(base, exp); - let mut expected_padded = vec![0u8; BN_SCALAR_LEN]; - let offset = BN_SCALAR_LEN - expected.len(); - expected_padded[offset..].copy_from_slice(&expected); - assert_eq!(actual, expected_padded, "base={base:?}, exp={exp:?}"); - } - - #[test] - fn test_is_bn254_fr() { - // Exact modulus - assert!(is_bn254_fr(&bn254_fr_modulus_be())); - - // With leading zeros - let mut padded = vec![0u8; 10]; - padded.extend_from_slice(&bn254_fr_modulus_be()); - assert!(is_bn254_fr(&padded)); - - // All zeros → false - assert!(!is_bn254_fr(&[0u8; 32])); - - // Wrong modulus (flip last bit) - let mut m = bn254_fr_modulus_be(); - *m.last_mut().unwrap() ^= 1; - assert!(!is_bn254_fr(&m)); - } - - #[test] - fn test_accelerated_modexp_bn254_fr() { - // --- short base (<=32 bytes), value < modulus --- - check(&[3], &[5]); // 3^5 mod Fr - check(&[0], &[5]); // 0^5 = 0 - check(&[3], &[0]); // 3^0 = 1 - check(&[0], &[0]); // 0^0 = 1 by convention - check(&[], &[]); // empty inputs - check(&[0, 0, 0, 3], &[5]); // leading zeros in base - - // --- short base, value >= modulus (triggers reduce fallback) --- - let m = bn254_fr_modulus_be(); - check(&m, &[1]); // Fr mod Fr = 0, so 0^1 = 0 - let mut m_plus_1 = m.clone(); - *m_plus_1.last_mut().unwrap() = m_plus_1.last().unwrap().wrapping_add(1); - check(&m_plus_1, &[2]); // (Fr+1)^2 mod Fr = 1 - check(&[0xff; 32], &[1]); // max 256-bit value, >= modulus - - // --- large base (> 32 bytes, reduce_be_bytes path) --- - check(&[0xab; 64], &[3]); // aligned (multiple of 32) - check(&[0x42; 100], &[2]); // unaligned (tests padding fix) - check(&[0xab; 64], &[0xff; 32]); // large base + large exponent - - // --- larger exponents --- - check(&[2], &[0xff; 32]); // 2^(2^256-1) mod Fr - check(&[2], &[0, 0, 0, 5]); // leading zeros in exponent - check(&[3], &[0xab; 64]); // exponent > 32 bytes - - // --- cross-path consistency: same value through different code paths --- - // 33-byte base with leading zero (reduce_be_bytes path) vs 32-byte base (from_be_bytes - // path) - let base_32 = [0xab; 32]; - let mut base_33 = vec![0u8]; - base_33.extend_from_slice(&base_32); - let exp = &[7]; - assert_eq!( - accelerated_modexp_bn254_fr(&base_32, exp), - accelerated_modexp_bn254_fr(&base_33, exp), - "33-byte base with leading zero must match 32-byte base" - ); - } - - /// Test the `Crypto::modexp` dispatch: accelerated path for BN254 Fr, - /// aurora fallback for other moduli. - #[test] - fn test_modexp_dispatch() { - let crypto = OpenVmCrypto; - let fr_mod = bn254_fr_modulus_be(); - - // Accelerated path: BN254 Fr modulus - let accel = crypto.modexp(&[3], &[5], &fr_mod).unwrap(); - let reference = reference_modexp(&[3], &[5], &fr_mod); - let mut ref_padded = vec![0u8; BN_SCALAR_LEN]; - let offset = BN_SCALAR_LEN - reference.len(); - ref_padded[offset..].copy_from_slice(&reference); - assert_eq!(accel, ref_padded, "accelerated path should match reference"); - - // Fallback path: non-BN254 modulus (e.g. small prime 7) - let other_mod = &[7]; - let fallback = crypto.modexp(&[3], &[4], other_mod).unwrap(); - let expected = reference_modexp(&[3], &[4], other_mod); - assert_eq!(fallback, expected, "fallback path should match reference"); - } +/// Install the OpenVM implementations globally. +pub fn install_openvm_crypto() -> Result> { + alloy::install()?; + Ok(revm::install()) } diff --git a/crates/revm-crypto/src/revm.rs b/crates/revm-crypto/src/revm.rs new file mode 100644 index 000000000..ba7468a8a --- /dev/null +++ b/crates/revm-crypto/src/revm.rs @@ -0,0 +1,391 @@ +//! REVM adapter for the standard zkVM accelerator C interface. + +use alloc::vec::Vec; +use core::mem::MaybeUninit; + +use crate::status_ok; +use openvm_accelerators::{ + zkvm_blake2f, zkvm_blake2f_message, zkvm_blake2f_offset, zkvm_blake2f_state, zkvm_bls12_381_fp, + zkvm_bls12_381_fp2, zkvm_bls12_381_g1_msm_pair, zkvm_bls12_381_g1_point, + zkvm_bls12_381_g2_msm_pair, zkvm_bls12_381_g2_point, zkvm_bls12_381_pairing_pair, + zkvm_bls12_381_scalar, zkvm_bls12_g1_add, zkvm_bls12_g1_msm, zkvm_bls12_g2_add, + zkvm_bls12_g2_msm, zkvm_bls12_map_fp2_to_g2, zkvm_bls12_map_fp_to_g1, zkvm_bls12_pairing, + zkvm_bn254_g1_add, zkvm_bn254_g1_mul, zkvm_bn254_g1_point, zkvm_bn254_g2_point, + zkvm_bn254_pairing, zkvm_bn254_pairing_pair, zkvm_bn254_scalar, zkvm_keccak256, + zkvm_keccak256_hash, zkvm_kzg_commitment, zkvm_kzg_field_element, zkvm_kzg_point_eval, + zkvm_kzg_proof, zkvm_modexp, zkvm_ripemd160, zkvm_ripemd160_hash, zkvm_secp256k1_ecrecover, + zkvm_secp256k1_hash, zkvm_secp256k1_pubkey, zkvm_secp256k1_signature, zkvm_secp256r1_hash, + zkvm_secp256r1_pubkey, zkvm_secp256r1_signature, zkvm_secp256r1_verify, zkvm_sha256, + zkvm_sha256_hash, +}; +use revm::precompile::{ + bls12_381::{ + G1Point as BlsG1Point, G1PointScalar as BlsG1PointScalar, G2Point as BlsG2Point, + G2PointScalar as BlsG2PointScalar, + }, + bls12_381_const::{FP_LENGTH as BLS_FP_LEN, G1_LENGTH as BLS_G1_LEN, G2_LENGTH as BLS_G2_LEN}, + Crypto, PrecompileHalt, +}; + +fn bls_g1((x, y): BlsG1Point) -> zkvm_bls12_381_g1_point { + let mut data = [0; BLS_G1_LEN]; + data[..BLS_FP_LEN].copy_from_slice(&x); + data[BLS_FP_LEN..].copy_from_slice(&y); + zkvm_bls12_381_g1_point { data } +} + +fn bls_g2((x0, x1, y0, y1): BlsG2Point) -> zkvm_bls12_381_g2_point { + let mut data = [0; BLS_G2_LEN]; + for (output, coordinate) in data.chunks_exact_mut(BLS_FP_LEN).zip([x0, x1, y0, y1]) { + output.copy_from_slice(&coordinate); + } + zkvm_bls12_381_g2_point { data } +} + +fn write_words(words: &[u64; N], bytes: &mut [u8]) { + for (output, word) in bytes.chunks_exact_mut(8).zip(words) { + output.copy_from_slice(&word.to_le_bytes()); + } +} + +#[derive(Debug, Default)] +struct OpenVmCrypto; + +impl Crypto for OpenVmCrypto { + fn sha256(&self, input: &[u8]) -> [u8; 32] { + let mut output = zkvm_sha256_hash { data: [0; 32] }; + let status = unsafe { zkvm_sha256(input.as_ptr(), input.len(), &mut output) }; + assert!(status_ok(status), "zkVM accelerator call failed"); + output.data + } + + fn ripemd160(&self, input: &[u8]) -> [u8; 32] { + let mut output = zkvm_ripemd160_hash { data: [0; 32] }; + let status = unsafe { zkvm_ripemd160(input.as_ptr(), input.len(), &mut output) }; + assert!(status_ok(status), "zkVM accelerator call failed"); + output.data + } + + fn bn254_g1_add(&self, p1: &[u8], p2: &[u8]) -> Result<[u8; 64], PrecompileHalt> { + let p1 = zkvm_bn254_g1_point { + data: p1.try_into().map_err(|_| PrecompileHalt::Bn254PairLength)?, + }; + let p2 = zkvm_bn254_g1_point { + data: p2.try_into().map_err(|_| PrecompileHalt::Bn254PairLength)?, + }; + let mut output = zkvm_bn254_g1_point { data: [0; 64] }; + let status = unsafe { zkvm_bn254_g1_add(&p1, &p2, &mut output) }; + if !status_ok(status) { + return Err(PrecompileHalt::Bn254AffineGFailedToCreate); + } + Ok(output.data) + } + + fn bn254_g1_mul(&self, point: &[u8], scalar: &[u8]) -> Result<[u8; 64], PrecompileHalt> { + let point = zkvm_bn254_g1_point { + data: point.try_into().map_err(|_| PrecompileHalt::Bn254PairLength)?, + }; + let scalar = zkvm_bn254_scalar { + data: scalar.try_into().map_err(|_| PrecompileHalt::Bn254PairLength)?, + }; + let mut output = zkvm_bn254_g1_point { data: [0; 64] }; + let status = unsafe { zkvm_bn254_g1_mul(&point, &scalar, &mut output) }; + if !status_ok(status) { + return Err(PrecompileHalt::Bn254AffineGFailedToCreate); + } + Ok(output.data) + } + + fn bn254_pairing_check(&self, pairs: &[(&[u8], &[u8])]) -> Result { + let pairs: Result, _> = pairs + .iter() + .map(|&(g1, g2)| { + Ok(zkvm_bn254_pairing_pair { + g1: zkvm_bn254_g1_point { + data: g1.try_into().map_err(|_| PrecompileHalt::Bn254PairLength)?, + }, + g2: zkvm_bn254_g2_point { + data: g2.try_into().map_err(|_| PrecompileHalt::Bn254PairLength)?, + }, + }) + }) + .collect(); + let pairs = pairs?; + let mut verified = false; + let status = unsafe { zkvm_bn254_pairing(pairs.as_ptr(), pairs.len(), &mut verified) }; + if !status_ok(status) { + return Err(PrecompileHalt::Bn254AffineGFailedToCreate); + } + Ok(verified) + } + + fn secp256k1_ecrecover( + &self, + sig: &[u8; 64], + recid: u8, + msg: &[u8; 32], + ) -> Result<[u8; 32], PrecompileHalt> { + let msg = zkvm_secp256k1_hash { data: *msg }; + let sig = zkvm_secp256k1_signature { data: *sig }; + let mut pubkey = zkvm_secp256k1_pubkey { data: [0; 64] }; + let status = unsafe { zkvm_secp256k1_ecrecover(&msg, &sig, recid, &mut pubkey) }; + if !status_ok(status) { + return Err(PrecompileHalt::Secp256k1RecoverFailed); + } + let mut hash = zkvm_keccak256_hash { data: [0; 32] }; + let status = unsafe { zkvm_keccak256(pubkey.data.as_ptr(), pubkey.data.len(), &mut hash) }; + assert!(status_ok(status), "zkVM accelerator call failed"); + hash.data[..12].fill(0); + Ok(hash.data) + } + + fn modexp(&self, base: &[u8], exp: &[u8], modulus: &[u8]) -> Result, PrecompileHalt> { + let mut output = alloc::vec![0; modulus.len()]; + let status = unsafe { + zkvm_modexp( + base.as_ptr(), + base.len(), + exp.as_ptr(), + exp.len(), + modulus.as_ptr(), + modulus.len(), + output.as_mut_ptr(), + ) + }; + assert!(status_ok(status), "zkVM accelerator call failed"); + Ok(output) + } + + fn blake2_compress(&self, rounds: u32, h: &mut [u64; 8], m: &[u64; 16], t: &[u64; 2], f: bool) { + let mut state = zkvm_blake2f_state { data: [0; 64] }; + let mut message = zkvm_blake2f_message { data: [0; 128] }; + let mut offset = zkvm_blake2f_offset { data: [0; 16] }; + write_words(h, &mut state.data); + write_words(m, &mut message.data); + write_words(t, &mut offset.data); + let status = unsafe { zkvm_blake2f(rounds, &mut state, &message, &offset, u8::from(f)) }; + assert!(status_ok(status), "zkVM accelerator call failed"); + for (word, bytes) in h.iter_mut().zip(state.data.as_chunks::<8>().0) { + *word = u64::from_le_bytes(*bytes); + } + } + + fn secp256r1_verify_signature(&self, msg: &[u8; 32], sig: &[u8; 64], pk: &[u8; 64]) -> bool { + let msg = zkvm_secp256r1_hash { data: *msg }; + let sig = zkvm_secp256r1_signature { data: *sig }; + let pubkey = zkvm_secp256r1_pubkey { data: *pk }; + let mut verified = false; + let status = unsafe { zkvm_secp256r1_verify(&msg, &sig, &pubkey, &mut verified) }; + status_ok(status) && verified + } + + fn verify_kzg_proof( + &self, + z: &[u8; 32], + y: &[u8; 32], + commitment: &[u8; 48], + proof: &[u8; 48], + ) -> Result<(), PrecompileHalt> { + let commitment = zkvm_kzg_commitment { data: *commitment }; + let z = zkvm_kzg_field_element { data: *z }; + let y = zkvm_kzg_field_element { data: *y }; + let proof = zkvm_kzg_proof { data: *proof }; + let mut verified = false; + let status = unsafe { zkvm_kzg_point_eval(&commitment, &z, &y, &proof, &mut verified) }; + if status_ok(status) && verified { + Ok(()) + } else { + Err(PrecompileHalt::BlobVerifyKzgProofFailed) + } + } + + fn bls12_381_g1_add( + &self, + a: BlsG1Point, + b: BlsG1Point, + ) -> Result<[u8; BLS_G1_LEN], PrecompileHalt> { + let (a, b) = (bls_g1(a), bls_g1(b)); + let mut output = MaybeUninit::::uninit(); + let status = unsafe { zkvm_bls12_g1_add(&a, &b, output.as_mut_ptr()) }; + if !status_ok(status) { + return Err(PrecompileHalt::Bls12381G1NotOnCurve); + } + // SAFETY: the C interface initializes the output when it returns success. + Ok(unsafe { output.assume_init() }.data) + } + + fn bls12_381_g1_msm( + &self, + pairs: &mut dyn Iterator>, + ) -> Result<[u8; BLS_G1_LEN], PrecompileHalt> { + let mut wire_pairs = Vec::with_capacity(pairs.size_hint().0); + for pair in pairs { + let (point, scalar) = pair?; + wire_pairs.push(zkvm_bls12_381_g1_msm_pair { + point: bls_g1(point), + scalar: zkvm_bls12_381_scalar { data: scalar }, + }); + } + let mut output = MaybeUninit::::uninit(); + let status = unsafe { + zkvm_bls12_g1_msm(wire_pairs.as_ptr(), wire_pairs.len(), output.as_mut_ptr()) + }; + if !status_ok(status) { + return Err(PrecompileHalt::Bls12381G1NotInSubgroup); + } + // SAFETY: the C interface initializes the output when it returns success. + Ok(unsafe { output.assume_init() }.data) + } + + fn bls12_381_g2_add( + &self, + a: BlsG2Point, + b: BlsG2Point, + ) -> Result<[u8; BLS_G2_LEN], PrecompileHalt> { + let (a, b) = (bls_g2(a), bls_g2(b)); + let mut output = MaybeUninit::::uninit(); + let status = unsafe { zkvm_bls12_g2_add(&a, &b, output.as_mut_ptr()) }; + if !status_ok(status) { + return Err(PrecompileHalt::Bls12381G2NotOnCurve); + } + // SAFETY: the C interface initializes the output when it returns success. + Ok(unsafe { output.assume_init() }.data) + } + + fn bls12_381_g2_msm( + &self, + pairs: &mut dyn Iterator>, + ) -> Result<[u8; BLS_G2_LEN], PrecompileHalt> { + let mut wire_pairs = Vec::with_capacity(pairs.size_hint().0); + for pair in pairs { + let (point, scalar) = pair?; + wire_pairs.push(zkvm_bls12_381_g2_msm_pair { + point: bls_g2(point), + scalar: zkvm_bls12_381_scalar { data: scalar }, + }); + } + let mut output = MaybeUninit::::uninit(); + let status = unsafe { + zkvm_bls12_g2_msm(wire_pairs.as_ptr(), wire_pairs.len(), output.as_mut_ptr()) + }; + if !status_ok(status) { + return Err(PrecompileHalt::Bls12381G2NotInSubgroup); + } + // SAFETY: the C interface initializes the output when it returns success. + Ok(unsafe { output.assume_init() }.data) + } + + fn bls12_381_pairing_check( + &self, + pairs: &[(BlsG1Point, BlsG2Point)], + ) -> Result { + let pairs: Vec<_> = pairs + .iter() + .copied() + .map(|(p1, p2)| zkvm_bls12_381_pairing_pair { g1: bls_g1(p1), g2: bls_g2(p2) }) + .collect(); + let mut verified = MaybeUninit::::uninit(); + let status = + unsafe { zkvm_bls12_pairing(pairs.as_ptr(), pairs.len(), verified.as_mut_ptr()) }; + if !status_ok(status) { + return Err(PrecompileHalt::Bls12381G1NotInSubgroup); + } + // SAFETY: the C interface initializes the output when it returns success. + Ok(unsafe { verified.assume_init() }) + } + + fn bls12_381_fp_to_g1( + &self, + fp: &[u8; BLS_FP_LEN], + ) -> Result<[u8; BLS_G1_LEN], PrecompileHalt> { + let fp = zkvm_bls12_381_fp { data: *fp }; + let mut output = MaybeUninit::::uninit(); + let status = unsafe { zkvm_bls12_map_fp_to_g1(&fp, output.as_mut_ptr()) }; + if !status_ok(status) { + return Err(PrecompileHalt::NonCanonicalFp); + } + // SAFETY: the C interface initializes the output when it returns success. + Ok(unsafe { output.assume_init() }.data) + } + + fn bls12_381_fp2_to_g2( + &self, + fp2: ([u8; BLS_FP_LEN], [u8; BLS_FP_LEN]), + ) -> Result<[u8; BLS_G2_LEN], PrecompileHalt> { + let mut data = [0; BLS_FP_LEN * 2]; + data[..BLS_FP_LEN].copy_from_slice(&fp2.0); + data[BLS_FP_LEN..].copy_from_slice(&fp2.1); + let fp2 = zkvm_bls12_381_fp2 { data }; + let mut output = MaybeUninit::::uninit(); + let status = unsafe { zkvm_bls12_map_fp2_to_g2(&fp2, output.as_mut_ptr()) }; + if !status_ok(status) { + return Err(PrecompileHalt::NonCanonicalFp); + } + // SAFETY: the C interface initializes the output when it returns success. + Ok(unsafe { output.assume_init() }.data) + } +} + +pub(super) fn install() -> bool { + revm::install_crypto(OpenVmCrypto) +} + +#[cfg(test)] +mod tests { + use super::*; + use revm::precompile::DefaultCrypto; + + #[test] + fn portable_operations_match_revm() { + let input = b"OpenVM accelerator C interface"; + assert_eq!(OpenVmCrypto.sha256(input), DefaultCrypto.sha256(input)); + assert_eq!(OpenVmCrypto.ripemd160(input), DefaultCrypto.ripemd160(input)); + assert_eq!( + OpenVmCrypto.modexp(&[0x12; 40], &[0x34; 3], &[0xef; 24]), + DefaultCrypto.modexp(&[0x12; 40], &[0x34; 3], &[0xef; 24]) + ); + + let mut actual = [ + 0x6a09e667f3bcc908, + 0xbb67ae8584caa73b, + 0x3c6ef372fe94f82b, + 0xa54ff53a5f1d36f1, + 0x510e527fade682d1, + 0x9b05688c2b3e6c1f, + 0x1f83d9abfb41bd6b, + 0x5be0cd19137e2179, + ]; + let mut expected = actual; + let message = [0x0123_4567_89ab_cdef; 16]; + let offset = [0x1020_3040_5060_7080, 0x90a0_b0c0_d0e0_f000]; + OpenVmCrypto.blake2_compress(12, &mut actual, &message, &offset, true); + DefaultCrypto.blake2_compress(12, &mut expected, &message, &offset, true); + assert_eq!(actual, expected); + } + + #[test] + fn p256_accepts_valid_and_rejects_invalid_signatures() { + let input = alloy_primitives::hex::decode("4cee90eb86eaa050036147a12d49004b6b9c72bd725d39d4785011fe190f0b4da73bd4903f0ce3b639bbbf6e8e80d16931ff4bcf5993d58468e8fb19086e8cac36dbcd03009df8c59286b162af3bd7fcc0450c9aa81be5d10d312af6c66b1d604aebd3099c618202fcfe16ae7770b0c49ab5eadf74b754204a3bb6060e44eff37618b065f9832de4ca6ca971a7a1adc826d0f7c00181a5fb2ddf79ae00b4e10e").unwrap(); + let msg = input[..32].try_into().unwrap(); + let signature = input[32..96].try_into().unwrap(); + let public_key = input[96..].try_into().unwrap(); + assert!(OpenVmCrypto.secp256r1_verify_signature(msg, signature, public_key)); + assert!(!OpenVmCrypto.secp256r1_verify_signature(msg, &[0; 64], public_key)); + } + + #[test] + fn c_status_failures_are_mapped_at_the_client_boundary() { + assert_eq!( + OpenVmCrypto.secp256k1_ecrecover(&[0; 64], 0, &[0; 32]), + Err(PrecompileHalt::Secp256k1RecoverFailed) + ); + assert_eq!( + OpenVmCrypto.bls12_381_fp_to_g1(&[0xff; BLS_FP_LEN]), + Err(PrecompileHalt::NonCanonicalFp) + ); + assert_eq!( + OpenVmCrypto.bn254_g1_add(&[0; 63], &[0; 64]), + Err(PrecompileHalt::Bn254PairLength) + ); + } +}