Skip to content
Merged
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
7 changes: 7 additions & 0 deletions rust/ffi/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@ fn main() {
if cfg!(target_os = "linux") {
println!("cargo:rustc-link-lib=dylib=resolv");
}
// On macOS the Go runtime reaches for CoreFoundation and Security for host lookups and the
// system certificate store. cgo does not emit these for a c-archive, so without them the link
// fails on undefined _CFArrayCreateMutable, _SecTrustEvaluate and friends.
if cfg!(target_os = "macos") {
println!("cargo:rustc-link-lib=framework=CoreFoundation");
println!("cargo:rustc-link-lib=framework=Security");
}

// Link the GPU stack fully static so the binary is self-contained: only system libs end up
// as NEEDED, and it runs with no LD_LIBRARY_PATH / ICICLE_BACKEND_INSTALL_DIR.
Expand Down
3 changes: 3 additions & 0 deletions rust/prover/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,6 @@ thiserror = "2"

[dev-dependencies]
ark-std = "0.5"
# For the prove_from_json example, which bridges live chain data into a proof.
hex = "0.4"
serde_json = "1"
87 changes: 87 additions & 0 deletions rust/prover/examples/prove_from_json.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
//! Generates an APK proof for a validator set collected from a live chain.
//!
//! Exists so a consumer does not have to depend on this crate to get a proof: the cgo toolchain and
//! the 800MB SRS stay here, and the data crosses as json.
//!
//! Input is `apk-inputs.json`, needing only two of its fields:
//!
//! ```json
//! { "keys": ["<96 byte hex G1, X||Y>", ...], "participation": [0, 1] }
//! ```
//!
//! Output is `apk-snark.json`, carrying the proof and the public inputs the Solidity verifier
//! wants, plus the bitlist and commitment read back out of those inputs so the caller does not
//! have to know their layout.
//!
//! cargo run --release --example prove_from_json -- /tmp/apk

use ark_bls12_381::{Fq, G1Affine};
use ark_ff::PrimeField;
use gnark_apk_prover::{ProofBuilder, ProverContext};
use std::{env, fs, time::Instant};

fn fq_from_be(bytes: &[u8]) -> Fq {
Fq::from_be_bytes_mod_order(bytes)
}

/// Inverse of the packing the consumer writes: 48 byte big-endian X then Y, no padding.
fn g1_from_packed(hex_str: &str) -> G1Affine {
let raw = hex::decode(hex_str).expect("key is hex");
assert_eq!(raw.len(), 96, "expected an uncompressed 96 byte G1 point");
let point = G1Affine::new_unchecked(fq_from_be(&raw[0..48]), fq_from_be(&raw[48..96]));
assert!(point.is_on_curve(), "key is not on the curve");
assert!(point.is_in_correct_subgroup_assuming_on_curve(), "key is not in the subgroup");
point
}

fn main() {
let dir = env::args().nth(1).expect("usage: prove_from_json <fixture dir>");
let inputs: serde_json::Value =
serde_json::from_str(&fs::read_to_string(format!("{dir}/apk-inputs.json")).expect("read"))
.expect("parse");

let keys: Vec<G1Affine> = inputs["keys"]
.as_array()
.expect("keys is an array")
.iter()
.map(|k| g1_from_packed(k.as_str().expect("key is a string")))
.collect();
let participation: Vec<u16> = inputs["participation"]
.as_array()
.expect("participation is an array")
.iter()
.map(|i| i.as_u64().expect("index is a number") as u16)
.collect();

println!("{} validator keys, {} signed", keys.len(), participation.len());

let started = Instant::now();
let ctx = ProverContext::setup(None).expect("setup");
println!("setup took {:?}", started.elapsed());

let started = Instant::now();
let proof = ProofBuilder::new(&ctx)
.public_keys(keys)
.participation(participation)
.prove()
.expect("prove");
println!("proving took {:?}", started.elapsed());

// The Solidity verifier takes 18 public inputs: the bitlist in the first five words, the
// commitment in the sixth, then the aggregate key as twelve limbs. The first six are what a
// caller has to pass alongside the proof, so hand them back decoded.
let raw = proof.public_inputs_calldata();
assert_eq!(raw.len(), 18 * 32, "expected 18 public inputs");
let word = |i: usize| hex::encode(&raw[i * 32..(i + 1) * 32]);

let out = serde_json::json!({
"apkProof": hex::encode(proof.proof_calldata()),
"publicInputs": hex::encode(raw),
"bitlist": (0..5).map(word).collect::<Vec<_>>(),
"apkCommitment": word(5),
});

let path = format!("{dir}/apk-snark.json");
fs::write(&path, serde_json::to_string_pretty(&out).unwrap()).expect("write");
println!("proof is {} bytes; wrote {path}", proof.proof_calldata().len());
}
118 changes: 118 additions & 0 deletions rust/prover/examples/prove_serve.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Copyright 2026 Polytope Labs.
// SPDX-License-Identifier: Apache-2.0
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Proves repeatedly, so the setup is paid once instead of per proof.
//!
//! `prove_from_json` compiles the circuit and generates the keys on every run, which is four
//! minutes a caller pays again and again. This does that once at startup and then answers
//! requests, one json object per line on stdin, one per line on stdout.
//!
//! Request: {"keys": ["<96 byte hex G1, X||Y>", ...], "participation": [0, 1]}
//! Response: {"apkProof": "..", "publicInputs": "..", "bitlist": [".."], "apkCommitment": ".."}
//! or {"error": "what went wrong"}
//!
//! The first line of stdout is `{"ready":true}`, so a caller knows the setup has finished.

use std::io::{BufRead, Write};

use ark_bls12_381::{Fq, G1Affine};
use ark_ff::PrimeField;
use gnark_apk_prover::{ProofBuilder, ProverContext};

fn g1_from_packed(hex_str: &str) -> Result<G1Affine, String> {
let raw = hex::decode(hex_str).map_err(|e| format!("key is not hex: {e}"))?;
if raw.len() != 96 {
return Err(format!("expected an uncompressed 96 byte G1 point, got {}", raw.len()));
}
let point = G1Affine::new_unchecked(
Fq::from_be_bytes_mod_order(&raw[0..48]),
Fq::from_be_bytes_mod_order(&raw[48..96]),
);
if !point.is_on_curve() || !point.is_in_correct_subgroup_assuming_on_curve() {
return Err("key is not a valid curve point".into());
}
Ok(point)
}

fn prove(
context: &ProverContext,
request: &serde_json::Value,
) -> Result<serde_json::Value, String> {
let keys = request["keys"]
.as_array()
.ok_or("request has no keys")?
.iter()
.map(|key| g1_from_packed(key.as_str().ok_or("key is not a string")?))
.collect::<Result<Vec<_>, _>>()?;
let participation = request["participation"]
.as_array()
.ok_or("request has no participation")?
.iter()
.map(|index| {
index
.as_u64()
.map(|i| i as u16)
.ok_or_else(|| "index is not a number".to_string())
})
.collect::<Result<Vec<_>, _>>()?;

let proof = ProofBuilder::new(context)
.public_keys(keys)
.participation(participation)
.prove()
.map_err(|e| format!("proving failed: {e}"))?;

let raw = proof.public_inputs_calldata();
if raw.len() != 18 * 32 {
return Err(format!("expected 18 public inputs, got {}", raw.len() / 32));
}
let word = |i: usize| hex::encode(&raw[i * 32..(i + 1) * 32]);

Ok(serde_json::json!({
"apkProof": hex::encode(proof.proof_calldata()),
"publicInputs": hex::encode(raw),
"bitlist": (0..5).map(word).collect::<Vec<_>>(),
"apkCommitment": word(5),
}))
}

fn main() {
let srs_dir = std::env::args().nth(1);
let context =
ProverContext::setup(srs_dir.as_deref().map(std::path::Path::new)).expect("setup");

let stdout = std::io::stdout();
let mut out = stdout.lock();
writeln!(out, "{}", serde_json::json!({ "ready": true })).expect("write");
out.flush().expect("flush");

for line in std::io::stdin().lock().lines() {
let line = line.expect("read");
if line.trim().is_empty() {
continue;
}

let response = match serde_json::from_str::<serde_json::Value>(&line) {
Ok(request) => match prove(&context, &request) {
Ok(proof) => proof,
Err(error) => serde_json::json!({ "error": error }),
},
Err(e) => serde_json::json!({ "error": format!("request is not json: {e}") }),
};

writeln!(out, "{response}").expect("write");
out.flush().expect("flush");
}
}
28 changes: 21 additions & 7 deletions rust/verifier/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,27 @@ version = "0.1.0"
edition = "2021"

[dependencies]
ark-bls12-381 = "0.5"
ark-ec = "0.5"
ark-ff = "0.5"
ark-serialize = "0.5"
sha2 = "0.10"
sha3 = "0.10"
thiserror = "2"
ark-bls12-381 = { version = "0.5", default-features = false, features = ["curve"] }
ark-ec = { version = "0.5", default-features = false }
ark-ff = { version = "0.5", default-features = false }
ark-serialize = { version = "0.5", default-features = false }
sha2 = { version = "0.10", default-features = false }
sha3 = { version = "0.10", default-features = false }
thiserror = { version = "2", default-features = false }
once_cell = { version = "1", default-features = false, features = ["alloc"] }

[features]
default = ["std"]
std = [
"ark-bls12-381/std",
"ark-ec/std",
"ark-ff/std",
"ark-serialize/std",
"sha2/std",
"sha3/std",
"thiserror/std",
"once_cell/std",
]

[dev-dependencies]
gnark-apk-prover = { path = "../prover", features = ["test-utils"] }
Expand Down
7 changes: 4 additions & 3 deletions rust/verifier/src/commitment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,14 @@
//! absorbing each limb separately, at a third of the compressions. See the
//! `LimbsPerElement` soundness note in `circuits/apk/apk.go`.

use alloc::{boxed::Box, vec::Vec};
use ark_bls12_381::{Fq, Fr, G1Affine};
use ark_ec::AffineRepr;
use ark_ff::{AdditiveGroup, BigInteger, Field, PrimeField};
use once_cell::race::OnceBox;

use crate::error::VerifierError;
use sha3::{Digest, Keccak256};
use std::sync::OnceLock;

// gnark-crypto default Poseidon2 parameters for BLS12-381 (compression / MD).
const WIDTH: usize = 2;
Expand All @@ -56,7 +57,7 @@ const SEED: &str = "Poseidon2-BLS12_381[t=2,rF=6,rP=50,d=5]";
/// `rndₖ₊₁ = Keccak(rndₖ)`, each key being `rnd mod r` (big-endian). Full rounds
/// carry `WIDTH` keys; partial rounds carry one (only lane 0 is keyed).
fn round_keys() -> &'static Vec<Vec<Fr>> {
static KEYS: OnceLock<Vec<Vec<Fr>>> = OnceLock::new();
static KEYS: OnceBox<Vec<Vec<Fr>>> = OnceBox::new();
KEYS.get_or_init(|| {
let half_full = FULL_ROUNDS / 2;
let total = FULL_ROUNDS + PARTIAL_ROUNDS;
Expand All @@ -72,7 +73,7 @@ fn round_keys() -> &'static Vec<Vec<Fr>> {
}
keys.push(row);
}
keys
Box::new(keys)
})
}

Expand Down
1 change: 1 addition & 0 deletions rust/verifier/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use alloc::string::String;
use thiserror::Error;

#[derive(Error, Debug)]
Expand Down
4 changes: 4 additions & 0 deletions rust/verifier/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@
//! # }
//! ```

#![cfg_attr(not(feature = "std"), no_std)]

extern crate alloc;

pub mod commitment;
pub mod error;
pub mod proof;
Expand Down
1 change: 1 addition & 0 deletions rust/verifier/src/proof.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
//! Proof bytes use gnark's `MarshalSolidity` layout (1184 bytes for 1 custom gate).
//! VK bytes use gnark's `WriteTo` binary format.

use alloc::vec::Vec;
use ark_bls12_381::{Fr, G1Affine, G2Affine};
use ark_ec::AffineRepr;
use ark_ff::{BigInteger256, BigInteger384, Field, PrimeField};
Expand Down
1 change: 1 addition & 0 deletions rust/verifier/src/transcript.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
//! The hash input starts at byte offset 0x1b (27) from the label — i.e. only the
//! last 5 bytes of the 32-byte label slot are included for 5-char labels.

use alloc::vec::Vec;
use ark_bls12_381::{Fq, Fr, G1Affine};
use ark_ec::AffineRepr;
use ark_ff::{Field, PrimeField};
Expand Down
1 change: 1 addition & 0 deletions rust/verifier/src/verifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
//! This is a direct translation of gnark's Solidity PLONK verifier into safe Rust
//! using arkworks BLS12-381 types.

use alloc::{format, vec, vec::Vec};
use ark_bls12_381::{Bls12_381, Fr, G1Affine, G1Projective};
use ark_ec::{pairing::Pairing, AffineRepr, CurveGroup, VariableBaseMSM};
use ark_ff::{Field, One, PrimeField, Zero};
Expand Down
Loading
Loading