Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gas-snapshot
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Groth16Bn254VerifierTest:testGas_bench_fe_verifyProof_ok() (gas: 202676)
Groth16Bn254VerifierTest:testGas_bench_fe_verifyProof_wrongPublicInput() (gas: 202754)
Groth16Bn254VerifierTest:testGas_bench_solidity_verifyProof_ok() (gas: 214887)
Groth16Bn254VerifierTest:testGas_bench_solidity_verifyProof_wrongPublicInput() (gas: 214921)
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/out/
/cache/
/broadcast/
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,14 @@
**Features**
- Groth16 Verifier
- BN254 Pairing Friendly Elliptic Curve
- Precompile Pairing Function
- Precompile Pairing Function

This repo is a **Fe v2 workspace** with two ingots:
- `crypto` (BN254 precompile wrappers)
- `verifiers` (Groth16 BN254 verifier)

Quick checks:
- `fe check .`
- `fe test ./crypto`
- `fe build --contract Groth16Bn254Verifier ./verifiers`
- `forge test`
171 changes: 171 additions & 0 deletions V2_UPDATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
# Fe v2 update (2026-01-26)

This document describes what changed in this repo during the “Fe v2” update, what was removed, and how to migrate any downstream usage.

## Summary

This repo started as older Fe v1-era code that:

- Implemented BN254 helpers using `std::precompiles` and `std::buf::MemoryBuffer`.
- Shipped a Groth16 verifier that built the pairing input via `MemoryBuffer` and called the pairing precompile.
- Included extra BN254 arithmetic (Fp/Fp2 ops, G2 arithmetic, hash-to-curve-ish helpers, and signing helpers).

The Fe v2 update refactors the repo to:

- Use the Fe v2 **workspace + ingot** structure.
- Use `std::evm::{alloc, ops}` and direct precompile `staticcall` wrappers for BN254 operations.
- Keep the Groth16 verifier, but express it in Fe v2 syntax and wire it to the BN254 wrappers.
- Remove artifacts that are not required to compile, test, or use the verifier library.

## Repository layout changes

### Before

- Two standalone “projects” with v1-style `fe.toml` manifests:
- `crypto/` (BN254 + helpers)
- `verifiers/` (Groth16 verifier + a `.fe.template`)

The v1 manifests were not compatible with the current Fe CLI (`fe 0.26.0`), and `fe check` failed with version parsing errors.

### After

- A single workspace root at `fe.toml` with two ingots:
- `crypto/`
- `verifiers/`

Files to look at:
- Workspace root: `fe.toml`
- Ingot manifests: `crypto/fe.toml`, `verifiers/fe.toml`
- Ingot entrypoints: `crypto/src/lib.fe`, `verifiers/src/lib.fe`

## BN254 implementation changes (`crypto` ingot)

### What was removed

The previous `crypto/src/curve/bn254.fe` contained far more than what the Groth16 verifier needs:

- Fp/Fp2 arithmetic helpers implemented via `modexp` and custom formulas.
- G2 curve arithmetic implemented in Fe.
- Hash/expand-message helpers and signing helpers (not appropriate for on-chain usage in most cases).

Those pieces were removed to keep the repo focused on on-chain verification via Ethereum precompiles.

### What replaced it

`crypto/src/bn254.fe` now provides **thin wrappers** around Ethereum’s BN254 precompiles:

- `0x06` ECADD
- `0x07` ECMUL
- `0x08` ECPAIRING

This is the same overall approach used in the `fe-verifiers` v2 codebase:

- Allocate input/output buffers with `std::evm::alloc`.
- Populate memory using `std::evm::ops::mstore`.
- Call precompiles with `std::evm::ops::staticcall`.
- Revert on failure via `std::evm::ops::revert(0, 0)`.

### G2 encoding detail (important)

The BN254 pairing precompile expects Fp2 coefficients in **reversed order** compared to the usual `(c0, c1)` math representation.

This repo stores `G2Point` as:

```
G2Point { x_c0, x_c1, y_c0, y_c1 }
```

…but when encoding for the pairing precompile, it writes:

```
x_c1, x_c0, y_c1, y_c0
```

That is handled internally by `store_pair` inside `crypto/src/bn254.fe`.

### Public API changes

Old code used:

- `std::precompiles::ec_add/ec_mul/ec_pairing`
- `std::buf::MemoryBuffer` for input packing
- Custom types like `Array<u256, 2>`

New code exposes:

- `G1Point` and `G2Point` structs (Fe v2 struct syntax)
- `negate`, `ec_add`, `ec_mul`
- `pairing_prod2/3/4` helpers that build the pairing input and perform the precompile call

`crypto/src/lib.fe` re-exports the BN254 module so consumers can `use crypto::bn254`.

## Groth16 verifier changes (`verifiers` ingot)

### What changed structurally

The Groth16 verifier implementation was moved into a dedicated module:

- `verifiers/src/groth16_bn254.fe`

`verifiers/src/lib.fe` re-exports it (so consumers can import `verifiers::groth16_bn254::*`) and includes a simple contract wrapper (`Groth16Bn254Verifier`).

### How the verifier works now

The verifier now:

- Validates the public input is `< SNARK_SCALAR_FIELD` and reverts otherwise.
- Computes `vk_x = IC[0] + IC[1] * public_input` using BN254 `ec_add`/`ec_mul`.
- Performs the Groth16 pairing product check using `bn254::pairing_prod4`.

### Compatibility function

`verifiers/src/groth16_bn254.fe` keeps a `verifyProof(a, b, c, input)` wrapper that matches the common SnarkJS call shape:

- `a` is `[u256; 2]` (G1)
- `b` is `[[u256; 2]; 2]` and is interpreted as `[[x_c1, x_c0], [y_c1, y_c0]]` (pairing precompile order)
- `c` is `[u256; 2]` (G1)
- `input` is `[u256; 1]` (this demo verifier has exactly one public input)

Internally it converts into the `Proof` struct and calls `verify`.

If you don’t need the SnarkJS-style wrapper, you can call `verify(Proof, public_input)` directly.

## Removals for minimalism

The following files were removed because they are not required to compile, test, or use the verifier library:

- `verifiers/src/main.fe` (example contract wrapper)
- `verifiers/src/groth16.fe.template` (EJS template convenience file)

If you want template-driven SnarkJS generation again, it can be reintroduced — but the v2 approach in `fe-verifiers` is typically “generate code, then paste constants”, rather than “swap the SnarkJS template”.

## How to validate the repo

From the workspace root:

- `fe check .`
- `fe test ./crypto`
- `fe build --contract Groth16Bn254Verifier ./verifiers`
- `forge test`

## Migration notes (v1 → v2)

If you had code importing the old v1 modules, the main changes are:

- Import paths changed:
- From `crypto::curve::bn254::...` to `crypto::bn254::...`
- Precompile usage changed:
- From `std::precompiles::*` + `MemoryBuffer` packing to `std::evm::{alloc, ops}` wrappers.
- Static arrays use Fe v2 syntax:
- From `Array<u256, N>` to `[u256; N]`

## What’s intentionally not included (yet)

This repo currently focuses on Groth16 verification on BN254 using Ethereum precompiles.

The “extra” cryptographic utilities from the v1 code (Fp/Fp2 arithmetic, G2 math, hash-to-curve-ish helpers, signing helpers) were removed to keep the codebase minimal and aligned with on-chain best practices.

If you want to bring more back in Fe v2 style, the adjacent `fe-verifiers` workspace includes examples for:

- KZG point evaluation precompile wrapper (EIP-4844)
- BLS12-381 precompile wrappers (EIP-2537)
8 changes: 3 additions & 5 deletions crypto/fe.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
# Ingot config
[ingot]
name = "crypto"
version = "1.0"

[dependencies]
# my_lib = "../my_lib"
# my_lib = { path = "../my_lib", version = "1.0" }
version = "0.1.0"
157 changes: 157 additions & 0 deletions crypto/src/bn254.fe
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
use std::evm::{alloc, ops}

const ECADD: u256 = 0x06
const ECMUL: u256 = 0x07
const ECPAIRING: u256 = 0x08

// BN254 base field modulus (Fp). Used for point negation.
const FP_MODULUS: u256 = 21888242871839275222246405745257275088696311157297823662689037894645226208583

pub struct G1Point { pub x: u256, pub y: u256 }

/// Fp2 element is represented as `c0 + c1 * i`.
///
/// IMPORTANT: The EVM pairing precompile expects Fp2 coefficients in reversed order, so
/// `x_c1` / `y_c1` are encoded before `x_c0` / `y_c0`.
pub struct G2Point { pub x_c0: u256, pub x_c1: u256, pub y_c0: u256, pub y_c1: u256 }

pub fn p1() -> G1Point {
G1Point { x: 1, y: 2 }
}

pub fn p2() -> G2Point {
G2Point {
x_c0: 10857046999023057135944570762232829481370756359578518086990519993285655852781,
x_c1: 11559732032986387107991004021392285783925812861821192530917403151452391805634,
y_c0: 8495653923123431417604973247489272438418190587263600148770280649306958101930,
y_c1: 4082367875863433681332203403145435568316851327593401208105741076214120093531,
}
}

pub fn negate(p: G1Point) -> G1Point {
if p.x == 0 && p.y == 0 {
return G1Point { x: 0, y: 0 }
}

let y = p.y % FP_MODULUS
let neg_y = if y == 0 { 0 } else { FP_MODULUS - y }
G1Point { x: p.x, y: neg_y }
}

pub fn ec_add(p1: G1Point, p2: G1Point) -> G1Point {
let in_ptr = alloc(128)
ops::mstore(in_ptr, p1.x)
ops::mstore(in_ptr + 32, p1.y)
ops::mstore(in_ptr + 64, p2.x)
ops::mstore(in_ptr + 96, p2.y)

let out_ptr = alloc(64)
let ok = ops::staticcall(
gas: ops::gas(),
addr: ECADD,
args_offset: in_ptr,
args_len: 128,
ret_offset: out_ptr,
ret_len: 64,
)
if ok == 0 {
ops::revert(0, 0)
}

G1Point {
x: ops::mload(out_ptr),
y: ops::mload(out_ptr + 32),
}
}

pub fn ec_mul(p: G1Point, s: u256) -> G1Point {
let in_ptr = alloc(96)
ops::mstore(in_ptr, p.x)
ops::mstore(in_ptr + 32, p.y)
ops::mstore(in_ptr + 64, s)

let out_ptr = alloc(64)
let ok = ops::staticcall(
gas: ops::gas(),
addr: ECMUL,
args_offset: in_ptr,
args_len: 96,
ret_offset: out_ptr,
ret_len: 64,
)
if ok == 0 {
ops::revert(0, 0)
}

G1Point {
x: ops::mload(out_ptr),
y: ops::mload(out_ptr + 32),
}
}

fn store_pair(base: u256, p1: G1Point, p2: G2Point) {
ops::mstore(base, p1.x)
ops::mstore(base + 32, p1.y)
// Reverse Fp2 coefficients for the precompile encoding.
ops::mstore(base + 64, p2.x_c1)
ops::mstore(base + 96, p2.x_c0)
ops::mstore(base + 128, p2.y_c1)
ops::mstore(base + 160, p2.y_c0)
}

fn pairing_call(in_ptr: u256, in_len: u256) -> bool {
let out_ptr = alloc(32)
let ok = ops::staticcall(
gas: ops::gas(),
addr: ECPAIRING,
args_offset: in_ptr,
args_len: in_len,
ret_offset: out_ptr,
ret_len: 32,
)
if ok == 0 {
ops::revert(0, 0)
}

ops::mload(out_ptr) != 0
}

pub fn pairing_prod2(a1: G1Point, a2: G2Point, b1: G1Point, b2: G2Point) -> bool {
let in_ptr = alloc(384)
store_pair(in_ptr, a1, a2)
store_pair(in_ptr + 192, b1, b2)
pairing_call(in_ptr, 384)
}

pub fn pairing_prod3(
a1: G1Point,
a2: G2Point,
b1: G1Point,
b2: G2Point,
c1: G1Point,
c2: G2Point,
) -> bool {
let in_ptr = alloc(576)
store_pair(in_ptr, a1, a2)
store_pair(in_ptr + 192, b1, b2)
store_pair(in_ptr + 384, c1, c2)
pairing_call(in_ptr, 576)
}

pub fn pairing_prod4(
a1: G1Point,
a2: G2Point,
b1: G1Point,
b2: G2Point,
c1: G1Point,
c2: G2Point,
d1: G1Point,
d2: G2Point,
) -> bool {
let in_ptr = alloc(768)
store_pair(in_ptr, a1, a2)
store_pair(in_ptr + 192, b1, b2)
store_pair(in_ptr + 384, c1, c2)
store_pair(in_ptr + 576, d1, d2)
pairing_call(in_ptr, 768)
}
Loading