Skip to content

Commit 39b227c

Browse files
authored
Merge pull request #6 from yugocabrio/feat/ec-trait
feat: EC trait that can switch to soroban-bn254-precompile
2 parents 09e1f44 + 2d76cc2 commit 39b227c

6 files changed

Lines changed: 271 additions & 166 deletions

File tree

.github/workflows/ci.yml

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ env:
1010
CARGO_TERM_COLOR: always
1111

1212
jobs:
13-
# ---------- rustfmt ----------
1413
lint:
1514
runs-on: ubuntu-latest
1615
steps:
@@ -20,24 +19,19 @@ jobs:
2019
components: rustfmt
2120
- run: cargo fmt --all -- --check
2221

23-
# ---------- Tests (independent) ----------
2422
test:
2523
runs-on: ubuntu-latest
2624

2725
steps:
2826
- uses: actions/checkout@v4
2927

30-
# Rust toolchain
3128
- uses: dtolnay/rust-toolchain@stable
3229

33-
# --- Pin legacy Noir toolchain (Poseidon2 still in stdlib) -----------
3430
- name: Install Noir toolchain 1.0.0-beta.3
3531
uses: noir-lang/noirup@v0.1.2
3632
with:
3733
toolchain: "1.0.0-beta.3"
38-
# --------------------------------------------------------------------
3934

40-
# Cache Cargo dependencies
4135
- uses: actions/cache@v3
4236
with:
4337
path: |
@@ -46,19 +40,17 @@ jobs:
4640
target
4741
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
4842

49-
# ---------- Build all Noir circuits ----------
5043
- name: Generate proofs & VKs
5144
run: tests/build_circuits.sh
5245

53-
# ---------- Run Rust tests ----------
54-
- name: Run tests
46+
- name: Run tests (default features)
5547
run: cargo test --verbose
5648

57-
# ---------- Test alloc feature ----------
58-
- name: Test alloc feature
59-
run: cargo test alloc --verbose
49+
- name: Run tests (std)
50+
run: cargo test --features std --verbose
6051

61-
# ---------- Test std feature ----------
62-
- name: Test std feature
63-
run: cargo test std --verbose
52+
- name: Run tests (soroban-bn254-precompile)
53+
run: cargo test --features soroban-bn254-precompile --verbose
6454

55+
- name: Run tests (std + soroban-bn254-precompile)
56+
run: cargo test --features "std soroban-bn254-precompile" --verbose

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ once_cell = { version = "1.19", default-features = false, features = ["alloc", "
2828

2929
[features]
3030
default = ["alloc", "serde", "serde_json"]
31+
# Soroban BN254 precompile integration switch
32+
soroban-bn254-precompile = []
3133
std = [
3234
"sha3/std",
3335
"ark-ff/std",

README.md

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# UltraHonk Rust Verifier 🦀
2-
Rust verifier library for proofs generated from Noir (UltraHonk) on BN254, with minimal dependencies. Its purpose is to verify Noir/UltraHonk proofs produced by Nargo + barretenberg (bb v0.87.0). A small Noir asset is included only for testing the verifier.
2+
Rust verifier library for proofs generated from Noir (UltraHonk) on BN254, with minimal dependencies. Its purpose is to verify Noir/UltraHonk proofs produced by Nargo 1.0.0-beta.9 + barretenberg (bb v0.87.0). A small Noir asset is included only for testing the verifier.
33

44
---
55

@@ -13,19 +13,17 @@ Rust verifier library for proofs generated from Noir (UltraHonk) on BN254, with
1313

1414
## Quick Start
1515
```bash
16-
# Run the verifier test against the included simple circuit
17-
cargo test --features "std" # quiet
16+
cargo test --features "std"
1817

19-
# With tracing output enabled (verbose, for debugging):
20-
cargo test --features "std,trace" -- --nocapture
18+
cargo test
2119
```
2220

2321
## How It Works
2422
- Typical pipeline: Noir circuit → Nargo prove → bb emits `proof`, `public_inputs`, `vk_fields.json` → this library verifies the proof.
2523
- Test data (already checked in) lives at `circuits/simple_circuit/target` and includes:
2624
- `proof` (raw bytes)
2725
- `public_inputs` (raw bytes) or `public_inputs_fields.json`
28-
- `vk_fields.json` (array of hex field elements; bb v0.87.0 layout)
26+
- `vk_fields.json` (array of hex field elements; Noir/Nargo 1.0.0-beta.9 + bb v0.87.0 layout)
2927
- The test at `tests/verifier_test.rs` loads these files and calls the Rust verifier.
3028

3129
---
@@ -37,7 +35,7 @@ Add the dependency from a git path or local path. The crate exposes a small API:
3735
```rust
3836
use ultrahonk_rust_verifier::UltraHonkVerifier;
3937

40-
// If you have a vk_fields.json string (bb v0.87.0)
38+
// If you have a vk_fields.json string (Noir/Nargo 1.0.0-beta.9, bb v0.87.0)
4139
let vk_json = std::fs::read_to_string("vk_fields.json").unwrap();
4240
let verifier = UltraHonkVerifier::new_from_json(&vk_json);
4341

@@ -53,15 +51,52 @@ verifier.verify(&proof_bytes, &public_inputs_bytes).unwrap();
5351
```
5452

5553
Notes:
56-
- Library scope: verification only (not a prover or circuit compiler). Input files must be produced by Noir/Nargo + bb v0.87.0.
54+
- Library scope: verification only (not a prover or circuit compiler). Input files must be produced by Noir/Nargo 1.0.0-beta.9 + bb v0.87.0.
5755
- The verifier internally re-derives the Fiat–Shamir transcript and checks both Sum‑check and Shplonk batch openings over BN254.
5856
- `std` feature enables file I/O and serde JSON; the core logic is `no_std` + `alloc` friendly.
5957
- Enable the `trace` feature to print step-by-step internals for cross‑checking with Solidity outputs.
6058

61-
## Features
59+
## Cargo Features
6260
- `std`: enables I/O and serde for convenient loading.
6361
- `trace`: prints detailed verifier internals (for debugging); off by default.
6462
- `alloc` (default): required for `no_std` collections.
63+
- `soroban-bn254-precompile`: routes MSM + pairing calls through a backend facade intended for a Soroban BN254 precompile.
64+
For now, it falls back to Arkworks so behavior is unchanged, but gives a stable call site to switch to host calls later.
65+
66+
### Soroban BN254 precompile
67+
- Purpose: Provide a seam to swap the EC hot paths (G1 MSM and pairing) to a Soroban BN254 precompile.
68+
- Enable: `--features soroban-bn254-precompile`. If no backend is registered, it transparently falls back to the Arkworks implementation.
69+
- Scope: Public API remains unchanged (`ec::g1_msm`, `ec::pairing_check`). Register a backend once at startup.
70+
71+
Backend contract
72+
- Trait: `ec::Bn254Ops` (intended to be `Send + Sync`)
73+
- `fn g1_msm(&self, coms: &[G1Point], scalars: &[Fr]) -> Result<G1Affine, String>`
74+
- Requirements: `coms.len() == scalars.len()`. G1 inputs are affine. Reject off-curve or wrong-subgroup points.
75+
- `fn pairing_check(&self, p0: &G1Affine, p1: &G1Affine) -> bool`
76+
- Must verify `e(p0, rhs_g2) * e(p1, lhs_g2) == 1` using the fixed G2 constants defined in `ec.rs`.
77+
78+
```
79+
#[cfg(feature = "soroban-bn254-precompile")]
80+
{
81+
use ultrahonk_rust_verifier::{ec::{self, Bn254Ops}, types::G1Point, field::Fr};
82+
use ark_bn254::G1Affine;
83+
84+
// Example backend that calls the Soroban host precompile (pseudo-code)
85+
struct SorobanOps { /* env: soroban_sdk::Env, ... */ }
86+
impl Bn254Ops for SorobanOps {
87+
fn g1_msm(&self, coms: &[G1Point], scalars: &[Fr]) -> Result<G1Affine, String> {
88+
// host_msm(env, coms, scalars).map_err(|e| e.to_string())
89+
unimplemented!("call Soroban MSM precompile")
90+
}
91+
fn pairing_check(&self, p0: &G1Affine, p1: &G1Affine) -> bool {
92+
// host_pairing_check(env, p0, p1)
93+
unimplemented!("call Soroban pairing precompile")
94+
}
95+
}
96+
97+
ec::set_soroban_bn254_backend(Box::new(SorobanOps { /* env, ... */ }));
98+
}
99+
```
65100

66101
---
67102

src/ec.rs

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
use crate::{field::Fr, types::G1Point};
2+
3+
#[cfg(not(feature = "std"))]
4+
use alloc::{boxed::Box, string::String};
5+
#[cfg(feature = "std")]
6+
use std::boxed::Box;
7+
8+
use crate::trace;
9+
use ark_bn254::{Bn254, Fq, Fq2, G1Affine, G1Projective, G2Affine};
10+
use ark_ec::{pairing::Pairing, CurveGroup, PrimeGroup};
11+
#[cfg(feature = "trace")]
12+
use ark_ff::BigInteger;
13+
use ark_ff::{One, PrimeField, Zero};
14+
15+
#[cfg(feature = "soroban-bn254-precompile")]
16+
use once_cell::race::OnceBox;
17+
18+
/// Trait for BN254 operations used by the verifier hot paths.
19+
/// Implement this to bridge MSM/pairing to a Soroban BN254 precompile.
20+
pub trait Bn254Ops {
21+
fn g1_msm(&self, coms: &[G1Point], scalars: &[Fr]) -> Result<G1Affine, String>;
22+
fn pairing_check(&self, p0: &G1Affine, p1: &G1Affine) -> bool;
23+
}
24+
25+
#[inline(always)]
26+
fn affine_checked(pt: &G1Point) -> Result<G1Affine, String> {
27+
let aff = G1Affine::new_unchecked(pt.x, pt.y);
28+
if aff.is_on_curve() && aff.is_in_correct_subgroup_assuming_on_curve() {
29+
Ok(aff)
30+
} else {
31+
Err("invalid G1 point (not on curve)".into())
32+
}
33+
}
34+
35+
#[inline(always)]
36+
fn negate(pt: &G1Point) -> G1Point {
37+
G1Point { x: pt.x, y: -pt.y }
38+
}
39+
40+
#[inline(always)]
41+
fn ark_g1_msm(coms: &[G1Point], scalars: &[Fr]) -> Result<G1Affine, String> {
42+
if coms.len() != scalars.len() {
43+
return Err("commitments / scalars length mismatch".into());
44+
}
45+
let mut acc = G1Projective::zero();
46+
trace!("Initial acc: {:?}", acc);
47+
for (c, s) in coms.iter().zip(scalars.iter()) {
48+
let aff = G1Affine::new_unchecked(c.x, c.y);
49+
if !aff.is_on_curve() || !aff.is_in_correct_subgroup_assuming_on_curve() {
50+
return Err("invalid G1 point".into());
51+
}
52+
#[cfg(feature = "trace")]
53+
{
54+
trace!(
55+
"Point.x = 0x{}",
56+
hex::encode(c.x.into_bigint().to_bytes_be())
57+
);
58+
trace!(
59+
"Point.y = 0x{}",
60+
hex::encode(c.y.into_bigint().to_bytes_be())
61+
);
62+
trace!("Scalar = 0x{}", hex::encode(s.to_bytes()));
63+
}
64+
acc += G1Projective::from(aff).mul_bigint(s.0.into_bigint());
65+
#[cfg(feature = "trace")]
66+
{
67+
let acc_aff = acc.into_affine();
68+
trace!(
69+
"Acc.x = 0x{}",
70+
hex::encode(acc_aff.x.into_bigint().to_bytes_be())
71+
);
72+
trace!(
73+
"Acc.y = 0x{}",
74+
hex::encode(acc_aff.y.into_bigint().to_bytes_be())
75+
);
76+
acc = G1Projective::from(acc_aff);
77+
}
78+
}
79+
Ok(acc.into_affine())
80+
}
81+
82+
#[inline(always)]
83+
fn ark_pairing_check(p0: &G1Affine, p1: &G1Affine) -> bool {
84+
// fixed RHS G2 (inputValues[2-5])
85+
let rhs_g2 = {
86+
let x = Fq2::new(
87+
Fq::from_le_bytes_mod_order(&[
88+
0xed, 0xf6, 0x92, 0xd9, 0x5c, 0xbd, 0xde, 0x46, 0xdd, 0xda, 0x5e, 0xf7, 0xd4, 0x22,
89+
0x43, 0x67, 0x79, 0x44, 0x5c, 0x5e, 0x66, 0x00, 0x6a, 0x42, 0x76, 0x1e, 0x1f, 0x12,
90+
0xef, 0xde, 0x00, 0x18,
91+
]),
92+
Fq::from_le_bytes_mod_order(&[
93+
0xc2, 0x12, 0xf3, 0xae, 0xb7, 0x85, 0xe4, 0x97, 0x12, 0xe7, 0xa9, 0x35, 0x33, 0x49,
94+
0xaa, 0xf1, 0x25, 0x5d, 0xfb, 0x31, 0xb7, 0xbf, 0x60, 0x72, 0x3a, 0x48, 0x0d, 0x92,
95+
0x93, 0x93, 0x8e, 0x19,
96+
]),
97+
);
98+
let y = Fq2::new(
99+
Fq::from_le_bytes_mod_order(&[
100+
0xaa, 0x7d, 0xfa, 0x66, 0x01, 0xcc, 0xe6, 0x4c, 0x7b, 0xd3, 0x43, 0x0c, 0x69, 0xe7,
101+
0xd1, 0xe3, 0x8f, 0x40, 0xcb, 0x8d, 0x80, 0x71, 0xab, 0x4a, 0xeb, 0x6d, 0x8c, 0xdb,
102+
0xa5, 0x5e, 0xc8, 0x12,
103+
]),
104+
Fq::from_le_bytes_mod_order(&[
105+
0x5b, 0x97, 0x22, 0xd1, 0xdc, 0xda, 0xac, 0x55, 0xf3, 0x8e, 0xb3, 0x70, 0x33, 0x31,
106+
0x4b, 0xbc, 0x95, 0x33, 0x0c, 0x69, 0xad, 0x99, 0x9e, 0xec, 0x75, 0xf0, 0x5f, 0x58,
107+
0xd0, 0x89, 0x06, 0x09,
108+
]),
109+
);
110+
G2Affine::new_unchecked(x, y)
111+
};
112+
// fixed LHS G2 (VK)
113+
let lhs_g2 = {
114+
let x = Fq2::new(
115+
Fq::from_le_bytes_mod_order(&[
116+
0xb0, 0x83, 0x88, 0x93, 0xec, 0x1f, 0x23, 0x7e, 0x8b, 0x07, 0x32, 0x3b, 0x07, 0x44,
117+
0x59, 0x9f, 0x4e, 0x97, 0xb5, 0x98, 0xb3, 0xb5, 0x89, 0xbc, 0xc2, 0xbc, 0x37, 0xb8,
118+
0xd5, 0xc4, 0x18, 0x01,
119+
]),
120+
Fq::from_le_bytes_mod_order(&[
121+
0xc1, 0x83, 0x93, 0xc0, 0xfa, 0x30, 0xfe, 0x4e, 0x8b, 0x03, 0x8e, 0x35, 0x7a, 0xd8,
122+
0x51, 0xea, 0xe8, 0xde, 0x91, 0x07, 0x58, 0x4e, 0xff, 0xe7, 0xc7, 0xf1, 0xf6, 0x51,
123+
0xb2, 0x01, 0x0e, 0x26,
124+
]),
125+
);
126+
let y = Fq2::new(
127+
Fq::from_le_bytes_mod_order(&[
128+
0x55, 0x5e, 0xcc, 0xda, 0xd4, 0x87, 0x4a, 0x85, 0xa2, 0xce, 0xe6, 0x96, 0x3f, 0xdd,
129+
0xe6, 0x11, 0x5e, 0x61, 0xe5, 0x14, 0x42, 0x5b, 0x47, 0x56, 0x2a, 0x63, 0xc0, 0xc0,
130+
0xa3, 0xbd, 0xfe, 0x22,
131+
]),
132+
Fq::from_le_bytes_mod_order(&[
133+
0xe4, 0x5f, 0x6a, 0xda, 0x80, 0x3c, 0x41, 0xee, 0xa4, 0x9b, 0xf9, 0x41, 0x46, 0xa0,
134+
0xf2, 0x9c, 0x85, 0x72, 0x9a, 0xbb, 0xc1, 0x56, 0x51, 0xd2, 0xe3, 0x0f, 0x11, 0xf7,
135+
0x69, 0x63, 0xfc, 0x04,
136+
]),
137+
);
138+
G2Affine::new_unchecked(x, y)
139+
};
140+
141+
let e1 = Bn254::pairing(*p0, rhs_g2);
142+
let e2 = Bn254::pairing(*p1, lhs_g2);
143+
e1.0 * e2.0 == <Bn254 as Pairing>::TargetField::one()
144+
}
145+
146+
pub struct ArkworksOps;
147+
148+
impl Bn254Ops for ArkworksOps {
149+
#[inline(always)]
150+
fn g1_msm(&self, coms: &[G1Point], scalars: &[Fr]) -> Result<G1Affine, String> {
151+
ark_g1_msm(coms, scalars)
152+
}
153+
#[inline(always)]
154+
fn pairing_check(&self, p0: &G1Affine, p1: &G1Affine) -> bool {
155+
ark_pairing_check(p0, p1)
156+
}
157+
}
158+
159+
static ARKWORKS: ArkworksOps = ArkworksOps;
160+
161+
#[cfg(feature = "soroban-bn254-precompile")]
162+
struct BackendHolder(pub Box<dyn Bn254Ops + Send + Sync>);
163+
164+
#[cfg(feature = "soroban-bn254-precompile")]
165+
static BACKEND: OnceBox<BackendHolder> = OnceBox::new();
166+
167+
#[inline(always)]
168+
fn backend() -> &'static dyn Bn254Ops {
169+
#[cfg(feature = "soroban-bn254-precompile")]
170+
{
171+
if let Some(b) = BACKEND.get() {
172+
return &*b.0;
173+
}
174+
}
175+
&ARKWORKS
176+
}
177+
178+
/// Multi-scalar multiplication on G1: ∑ sᵢ·Cᵢ
179+
#[inline(always)]
180+
pub fn g1_msm(coms: &[G1Point], scalars: &[Fr]) -> Result<G1Affine, String> {
181+
backend().g1_msm(coms, scalars)
182+
}
183+
184+
/// Pairing product check e(P0, rhs_g2) * e(P1, lhs_g2) == 1
185+
#[inline(always)]
186+
pub fn pairing_check(p0: &G1Affine, p1: &G1Affine) -> bool {
187+
backend().pairing_check(p0, p1)
188+
}
189+
190+
pub mod helpers {
191+
use super::*;
192+
#[inline(always)]
193+
pub fn affine_checked(pt: &G1Point) -> Result<G1Affine, String> {
194+
super::affine_checked(pt)
195+
}
196+
#[inline(always)]
197+
pub fn negate(pt: &G1Point) -> G1Point {
198+
super::negate(pt)
199+
}
200+
}
201+
202+
#[cfg(feature = "soroban-bn254-precompile")]
203+
/// Register a custom BN254 backend (Soroban BN254 precompile bridge).
204+
pub fn set_backend(ops: Box<dyn Bn254Ops + Send + Sync>) {
205+
let _ = BACKEND.set(Box::new(BackendHolder(ops)));
206+
}
207+
208+
#[cfg(feature = "soroban-bn254-precompile")]
209+
#[inline(always)]
210+
pub fn set_soroban_bn254_backend(ops: Box<dyn Bn254Ops + Send + Sync>) {
211+
set_backend(ops)
212+
}

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
extern crate alloc;
55

66
pub mod debug;
7+
pub mod ec;
78
pub mod field;
89
pub mod hash;
910
pub mod relations;

0 commit comments

Comments
 (0)