From 5085525e484b37d9f063117c3eb56e381131f5e8 Mon Sep 17 00:00:00 2001
From: Matt Stam <15695189+mattstam@users.noreply.github.com>
Date: Fri, 16 Aug 2024 18:37:18 -0700
Subject: [PATCH] feat: add groth16 (#1313)

---
 book/developers/building-plonk-artifacts.md   |   8 +-
 crates/prover/Cargo.toml                      |   4 +
 crates/prover/Makefile                        |  11 +-
 crates/prover/scripts/build_groth16_bn254.rs  |  18 ++
 crates/prover/scripts/e2e.rs                  |  37 +++-
 crates/prover/src/build.rs                    |  42 +++-
 crates/prover/src/init.rs                     |   3 +-
 crates/prover/src/lib.rs                      |  51 ++++-
 crates/prover/src/types.rs                    |  49 ++++-
 crates/prover/src/verify.rs                   |  60 +++++-
 crates/recursion/gnark-cli/src/main.rs        |  81 ++++++--
 .../gnark-ffi/assets/SP1Verifier.txt          |   6 +-
 crates/recursion/gnark-ffi/go/.gitignore      |   2 +-
 crates/recursion/gnark-ffi/go/main.go         |  72 ++++++-
 crates/recursion/gnark-ffi/go/sp1/build.go    | 113 ++++++++++-
 crates/recursion/gnark-ffi/go/sp1/prove.go    |  83 +++++++-
 crates/recursion/gnark-ffi/go/sp1/sp1.go      |  14 +-
 crates/recursion/gnark-ffi/go/sp1/utils.go    |  23 +++
 crates/recursion/gnark-ffi/go/sp1/verify.go   |  51 ++++-
 crates/recursion/gnark-ffi/src/ffi/docker.rs  | 120 +++++++++---
 crates/recursion/gnark-ffi/src/ffi/native.rs  | 184 ++++++++++++++----
 .../recursion/gnark-ffi/src/groth16_bn254.rs  | 126 ++++++++++++
 crates/recursion/gnark-ffi/src/lib.rs         |   5 +-
 crates/recursion/gnark-ffi/src/plonk_bn254.rs |  20 +-
 crates/recursion/gnark-ffi/src/proof.rs       |  25 +++
 crates/sdk/src/artifacts.rs                   |  47 ++++-
 crates/sdk/src/install.rs                     |  42 ++--
 crates/sdk/src/network/prover.rs              |   1 +
 crates/sdk/src/proof.rs                       |  15 +-
 crates/sdk/src/proto/network.rs               |  16 +-
 crates/sdk/src/provers/local.rs               |  38 +++-
 crates/sdk/src/provers/mock.rs                |  26 ++-
 crates/sdk/src/provers/mod.rs                 |  27 ++-
 33 files changed, 1215 insertions(+), 205 deletions(-)
 create mode 100644 crates/prover/scripts/build_groth16_bn254.rs
 create mode 100644 crates/recursion/gnark-ffi/src/groth16_bn254.rs
 create mode 100644 crates/recursion/gnark-ffi/src/proof.rs

diff --git a/book/developers/building-plonk-artifacts.md b/book/developers/building-plonk-artifacts.md
index fd0ba2a68b..8f7eb5ad16 100644
--- a/book/developers/building-plonk-artifacts.md
+++ b/book/developers/building-plonk-artifacts.md
@@ -1,8 +1,8 @@
-# Building PLONK Artifacts
+# Building Circuit Artifacts
 
-To build the production Plonk Bn254 artifacts from scratch, you can use the `Makefile` inside the `prover` directory.
+To build the production PLONK and Groth16 Bn254 artifacts from scratch, you can use the `Makefile` inside the `prover` directory.
 
 ```shell,noplayground
 cd prover
-RUST_LOG=info make build-plonk-bn254
-```
\ No newline at end of file
+RUST_LOG=info make build-circuits
+```
diff --git a/crates/prover/Cargo.toml b/crates/prover/Cargo.toml
index 65ec133348..5ec658f832 100644
--- a/crates/prover/Cargo.toml
+++ b/crates/prover/Cargo.toml
@@ -47,6 +47,10 @@ oneshot = "0.1.8"
 name = "build_plonk_bn254"
 path = "scripts/build_plonk_bn254.rs"
 
+[[bin]]
+name = "build_groth16_bn254"
+path = "scripts/build_groth16_bn254.rs"
+
 [[bin]]
 name = "e2e"
 path = "scripts/e2e.rs"
diff --git a/crates/prover/Makefile b/crates/prover/Makefile
index 8ab442e38b..9365179916 100644
--- a/crates/prover/Makefile
+++ b/crates/prover/Makefile
@@ -1,15 +1,18 @@
 all:
-	make build-plonk-bn254
-	make release-plonk-bn254
+	make build-circuits
+	make release-circuits
 
-build-plonk-bn254:
+build-circuits:
 	rm -rf build && \
 	mkdir -p build && \
 	RUSTFLAGS='-C target-cpu=native' \
 	cargo run -p sp1-prover --release --bin build_plonk_bn254 --features native-gnark -- \
+	--build-dir=./build && \
+	RUSTFLAGS='-C target-cpu=native' \
+	cargo run -p sp1-prover --release --bin build_groth16_bn254 --features native-gnark -- \
 	--build-dir=./build
 
-release-plonk-bn254:
+release-circuits:
 	@read -p "Release version (ex. v1.0.0-testnet)? " version; \
 	bash release.sh $$version
 
diff --git a/crates/prover/scripts/build_groth16_bn254.rs b/crates/prover/scripts/build_groth16_bn254.rs
new file mode 100644
index 0000000000..e7b5e1e61f
--- /dev/null
+++ b/crates/prover/scripts/build_groth16_bn254.rs
@@ -0,0 +1,18 @@
+use std::path::PathBuf;
+
+use clap::Parser;
+use sp1_core_machine::utils::setup_logger;
+use sp1_prover::build::build_groth16_bn254_artifacts_with_dummy;
+
+#[derive(Parser, Debug)]
+#[clap(author, version, about, long_about = None)]
+struct Args {
+    #[clap(short, long)]
+    build_dir: PathBuf,
+}
+
+pub fn main() {
+    setup_logger();
+    let args = Args::parse();
+    build_groth16_bn254_artifacts_with_dummy(args.build_dir);
+}
diff --git a/crates/prover/scripts/e2e.rs b/crates/prover/scripts/e2e.rs
index 6b40b66d86..5cd757e559 100644
--- a/crates/prover/scripts/e2e.rs
+++ b/crates/prover/scripts/e2e.rs
@@ -12,6 +12,7 @@ use sp1_prover::{
 use sp1_recursion_circuit::{stark::build_wrap_circuit, witness::Witnessable};
 use sp1_recursion_compiler::ir::Witness;
 use sp1_recursion_core::air::RecursionPublicValues;
+use sp1_recursion_gnark_ffi::Groth16Bn254Prover;
 use sp1_recursion_gnark_ffi::PlonkBn254Prover;
 use sp1_stark::SP1ProverOpts;
 use subtle_encoding::hex;
@@ -21,6 +22,8 @@ use subtle_encoding::hex;
 struct Args {
     #[clap(short, long)]
     build_dir: String,
+    #[arg(short, long)]
+    system: String,
 }
 
 pub fn main() {
@@ -69,19 +72,19 @@ pub fn main() {
     witness.write_commited_values_digest(committed_values_digest);
     witness.write_vkey_hash(vkey_hash);
 
-    tracing::info!("sanity check gnark test");
+    tracing::info!("sanity check plonk test");
     PlonkBn254Prover::test(constraints.clone(), witness.clone());
 
-    tracing::info!("sanity check gnark build");
+    tracing::info!("sanity check plonk build");
     PlonkBn254Prover::build(constraints.clone(), witness.clone(), build_dir.clone());
 
-    tracing::info!("sanity check gnark prove");
+    tracing::info!("sanity check plonk prove");
     let plonk_bn254_prover = PlonkBn254Prover::new();
 
-    tracing::info!("gnark prove");
+    tracing::info!("plonk prove");
     let proof = plonk_bn254_prover.prove(witness.clone(), build_dir.clone());
 
-    tracing::info!("verify gnark proof");
+    tracing::info!("verify plonk proof");
     plonk_bn254_prover.verify(
         &proof,
         &vkey_hash.as_canonical_biguint(),
@@ -89,5 +92,27 @@ pub fn main() {
         &build_dir,
     );
 
-    println!("{:?}", String::from_utf8(hex::encode(proof.encoded_proof)).unwrap());
+    println!("plonk proof: {:?}", String::from_utf8(hex::encode(proof.encoded_proof)).unwrap());
+
+    tracing::info!("sanity check groth16 test");
+    Groth16Bn254Prover::test(constraints.clone(), witness.clone());
+
+    tracing::info!("sanity check groth16 build");
+    Groth16Bn254Prover::build(constraints.clone(), witness.clone(), build_dir.clone());
+
+    tracing::info!("sanity check groth16 prove");
+    let groth16_bn254_prover = Groth16Bn254Prover::new();
+
+    tracing::info!("groth16 prove");
+    let proof = groth16_bn254_prover.prove(witness.clone(), build_dir.clone());
+
+    tracing::info!("verify groth16 proof");
+    groth16_bn254_prover.verify(
+        &proof,
+        &vkey_hash.as_canonical_biguint(),
+        &committed_values_digest.as_canonical_biguint(),
+        &build_dir,
+    );
+
+    println!("groth16 proof: {:?}", String::from_utf8(hex::encode(proof.encoded_proof)).unwrap());
 }
diff --git a/crates/prover/src/build.rs b/crates/prover/src/build.rs
index d0406395f1..d15da809b4 100644
--- a/crates/prover/src/build.rs
+++ b/crates/prover/src/build.rs
@@ -8,7 +8,7 @@ pub use sp1_recursion_compiler::ir::Witness;
 use sp1_recursion_compiler::{config::OuterConfig, constraints::Constraint};
 use sp1_recursion_core::air::RecursionPublicValues;
 pub use sp1_recursion_core::stark::utils::sp1_dev_mode;
-use sp1_recursion_gnark_ffi::PlonkBn254Prover;
+use sp1_recursion_gnark_ffi::{Groth16Bn254Prover, PlonkBn254Prover};
 use sp1_stark::{SP1ProverOpts, ShardProof, StarkVerifyingKey};
 
 use crate::{
@@ -27,9 +27,25 @@ pub fn try_build_plonk_bn254_artifacts_dev(
     build_dir
 }
 
+/// Tries to build the groth16 bn254 artifacts in the current environment.
+pub fn try_build_groth16_bn254_artifacts_dev(
+    template_vk: &StarkVerifyingKey<OuterSC>,
+    template_proof: &ShardProof<OuterSC>,
+) -> PathBuf {
+    let build_dir = groth16_bn254_artifacts_dev_dir();
+    println!("[sp1] building groth16 bn254 artifacts in development mode");
+    build_groth16_bn254_artifacts(template_vk, template_proof, &build_dir);
+    build_dir
+}
+
 /// Gets the directory where the PLONK artifacts are installed in development mode.
 pub fn plonk_bn254_artifacts_dev_dir() -> PathBuf {
-    dirs::home_dir().unwrap().join(".sp1").join("circuits").join("plonk_bn254").join("dev")
+    dirs::home_dir().unwrap().join(".sp1").join("circuits").join("dev")
+}
+
+/// Gets the directory where the groth16 artifacts are installed in development mode.
+pub fn groth16_bn254_artifacts_dev_dir() -> PathBuf {
+    dirs::home_dir().unwrap().join(".sp1").join("circuits").join("dev")
 }
 
 /// Build the plonk bn254 artifacts to the given directory for the given verification key and
@@ -45,6 +61,19 @@ pub fn build_plonk_bn254_artifacts(
     PlonkBn254Prover::build(constraints, witness, build_dir);
 }
 
+/// Build the groth16 bn254 artifacts to the given directory for the given verification key and template
+/// proof.
+pub fn build_groth16_bn254_artifacts(
+    template_vk: &StarkVerifyingKey<OuterSC>,
+    template_proof: &ShardProof<OuterSC>,
+    build_dir: impl Into<PathBuf>,
+) {
+    let build_dir = build_dir.into();
+    std::fs::create_dir_all(&build_dir).expect("failed to create build directory");
+    let (constraints, witness) = build_constraints_and_witness(template_vk, template_proof);
+    Groth16Bn254Prover::build(constraints, witness, build_dir);
+}
+
 /// Builds the plonk bn254 artifacts to the given directory.
 ///
 /// This may take a while as it needs to first generate a dummy proof and then it needs to compile
@@ -54,6 +83,15 @@ pub fn build_plonk_bn254_artifacts_with_dummy(build_dir: impl Into<PathBuf>) {
     crate::build::build_plonk_bn254_artifacts(&wrap_vk, &wrapped_proof, build_dir.into());
 }
 
+/// Builds the groth16 bn254 artifacts to the given directory.
+///
+/// This may take a while as it needs to first generate a dummy proof and then it needs to compile
+/// the circuit.
+pub fn build_groth16_bn254_artifacts_with_dummy(build_dir: impl Into<PathBuf>) {
+    let (wrap_vk, wrapped_proof) = dummy_proof();
+    crate::build::build_groth16_bn254_artifacts(&wrap_vk, &wrapped_proof, build_dir.into());
+}
+
 /// Build the verifier constraints and template witness for the circuit.
 pub fn build_constraints_and_witness(
     template_vk: &StarkVerifyingKey<OuterSC>,
diff --git a/crates/prover/src/init.rs b/crates/prover/src/init.rs
index f173206456..6f79d898a3 100644
--- a/crates/prover/src/init.rs
+++ b/crates/prover/src/init.rs
@@ -4,7 +4,8 @@ pub use sp1_core_machine::io::{SP1PublicValues, SP1Stdin};
 use sp1_primitives::types::RecursionProgramType;
 use sp1_recursion_compiler::config::InnerConfig;
 use sp1_recursion_core::runtime::RecursionProgram;
-pub use sp1_recursion_gnark_ffi::plonk_bn254::PlonkBn254Proof;
+pub use sp1_recursion_gnark_ffi::groth16_bn254::Groth16Bn254Prover;
+pub use sp1_recursion_gnark_ffi::plonk_bn254::PlonkBn254Prover;
 pub use sp1_recursion_program::machine::{
     ReduceProgramType, SP1CompressMemoryLayout, SP1DeferredMemoryLayout, SP1RecursionMemoryLayout,
     SP1RootMemoryLayout,
diff --git a/crates/prover/src/lib.rs b/crates/prover/src/lib.rs
index d719a7cd63..27088cb51a 100644
--- a/crates/prover/src/lib.rs
+++ b/crates/prover/src/lib.rs
@@ -45,8 +45,10 @@ use sp1_recursion_core::{
     runtime::{ExecutionRecord, RecursionProgram, Runtime as RecursionRuntime},
     stark::{config::BabyBearPoseidon2Outer, RecursionAir},
 };
-pub use sp1_recursion_gnark_ffi::plonk_bn254::PlonkBn254Proof;
+use sp1_recursion_gnark_ffi::groth16_bn254::Groth16Bn254Prover;
 use sp1_recursion_gnark_ffi::plonk_bn254::PlonkBn254Prover;
+pub use sp1_recursion_gnark_ffi::proof::Groth16Bn254Proof;
+pub use sp1_recursion_gnark_ffi::proof::PlonkBn254Proof;
 use sp1_recursion_program::hints::Hintable;
 pub use sp1_recursion_program::machine::{
     ReduceProgramType, SP1CompressMemoryLayout, SP1DeferredMemoryLayout, SP1RecursionMemoryLayout,
@@ -860,6 +862,35 @@ impl<C: SP1ProverComponents> SP1Prover<C> {
         proof
     }
 
+    /// Wrap the STARK proven over a SNARK-friendly field into a Groth16 proof.
+    #[instrument(name = "wrap_groth16_bn254", level = "info", skip_all)]
+    pub fn wrap_groth16_bn254(
+        &self,
+        proof: SP1ReduceProof<OuterSC>,
+        build_dir: &Path,
+    ) -> Groth16Bn254Proof {
+        let vkey_digest = proof.sp1_vkey_digest_bn254();
+        let commited_values_digest = proof.sp1_commited_values_digest_bn254();
+
+        let mut witness = Witness::default();
+        proof.proof.write(&mut witness);
+        witness.write_commited_values_digest(commited_values_digest);
+        witness.write_vkey_hash(vkey_digest);
+
+        let prover = Groth16Bn254Prover::new();
+        let proof = prover.prove(witness, build_dir.to_path_buf());
+
+        // Verify the proof.
+        prover.verify(
+            &proof,
+            &vkey_digest.as_canonical_biguint(),
+            &commited_values_digest.as_canonical_biguint(),
+            build_dir,
+        );
+
+        proof
+    }
+
     /// Accumulate deferred proofs into a single digest.
     pub fn hash_deferred_proofs(
         prev_digest: [Val<CoreSC>; DIGEST_SIZE],
@@ -881,7 +912,7 @@ impl<C: SP1ProverComponents> SP1Prover<C> {
     fn check_for_high_cycles(cycles: u64) {
         if cycles > 100_000_000 {
             tracing::warn!(
-                "high cycle count, consider using the prover network for proof generation: https://docs.succinct.xyz/prover-network/setup.html"
+                "high cycle count, consider using the prover network for proof generation: https://docs.succinct.xyz/generating-proofs/prover-network"
             );
         }
     }
@@ -898,6 +929,7 @@ pub mod tests {
     use super::*;
 
     use anyhow::Result;
+    use build::try_build_groth16_bn254_artifacts_dev;
     use build::try_build_plonk_bn254_artifacts_dev;
     use p3_field::PrimeField32;
     use sp1_core_machine::io::SP1Stdin;
@@ -993,11 +1025,20 @@ pub mod tests {
         tracing::info!("generate plonk bn254 proof");
         let artifacts_dir =
             try_build_plonk_bn254_artifacts_dev(prover.wrap_vk(), &wrapped_bn254_proof.proof);
-        let plonk_bn254_proof = prover.wrap_plonk_bn254(wrapped_bn254_proof, &artifacts_dir);
+        let plonk_bn254_proof =
+            prover.wrap_plonk_bn254(wrapped_bn254_proof.clone(), &artifacts_dir);
         println!("{:?}", plonk_bn254_proof);
 
         prover.verify_plonk_bn254(&plonk_bn254_proof, &vk, &public_values, &artifacts_dir)?;
 
+        tracing::info!("generate groth16 bn254 proof");
+        let artifacts_dir =
+            try_build_groth16_bn254_artifacts_dev(prover.wrap_vk(), &wrapped_bn254_proof.proof);
+        let groth16_bn254_proof = prover.wrap_groth16_bn254(wrapped_bn254_proof, &artifacts_dir);
+        println!("{:?}", groth16_bn254_proof);
+
+        prover.verify_groth16_bn254(&groth16_bn254_proof, &vk, &public_values, &artifacts_dir)?;
+
         Ok(())
     }
 
@@ -1093,7 +1134,9 @@ pub mod tests {
         let elf = include_bytes!("../../../tests/fibonacci/elf/riscv32im-succinct-zkvm-elf");
         setup_logger();
         let opts = SP1ProverOpts::default();
-        test_e2e_prover::<DefaultProverComponents>(elf, opts, Test::Plonk)
+        // TODO(mattstam): We should Test::Plonk here, but this uses the existing
+        // docker image which has a different API than the current. So we need to wait until the next release (v1.2.0+), and then switch it back.
+        test_e2e_prover::<DefaultProverComponents>(elf, opts, Test::Wrap)
     }
 
     /// Tests an end-to-end workflow of proving a program across the entire proof generation
diff --git a/crates/prover/src/types.rs b/crates/prover/src/types.rs
index 93da49ac04..95bc5d8a6c 100644
--- a/crates/prover/src/types.rs
+++ b/crates/prover/src/types.rs
@@ -12,7 +12,7 @@ use sp1_core_machine::{
 };
 use sp1_primitives::poseidon2_hash;
 use sp1_recursion_core::{air::RecursionPublicValues, stark::config::BabyBearPoseidon2Outer};
-use sp1_recursion_gnark_ffi::plonk_bn254::PlonkBn254Proof;
+use sp1_recursion_gnark_ffi::proof::{Groth16Bn254Proof, PlonkBn254Proof};
 use sp1_recursion_program::machine::{
     SP1CompressMemoryLayout, SP1DeferredMemoryLayout, SP1RecursionMemoryLayout,
 };
@@ -144,11 +144,15 @@ pub type SP1ReducedProof = SP1ProofWithMetadata<SP1ReducedProofData>;
 /// An SP1 proof that has been wrapped into a single PLONK proof and can be verified onchain.
 pub type SP1PlonkBn254Proof = SP1ProofWithMetadata<SP1PlonkBn254ProofData>;
 
-/// An SP1 proof that has been wrapped into a single PLONK proof and can be verified onchain.
-pub type SP1PlonkProof = SP1ProofWithMetadata<SP1PlonkProofData>;
+/// An SP1 proof that has been wrapped into a single Groth16 proof and can be verified onchain.
+pub type SP1Groth16Bn254Proof = SP1ProofWithMetadata<SP1Groth16Bn254ProofData>;
+
+/// An SP1 proof that has been wrapped into a single proof and can be verified onchain.
+pub type SP1Proof = SP1ProofWithMetadata<SP1Bn254ProofData>;
 
 #[derive(Serialize, Deserialize, Clone)]
 pub struct SP1CoreProofData(pub Vec<ShardProof<CoreSC>>);
+
 #[derive(Serialize, Deserialize, Clone)]
 pub struct SP1ReducedProofData(pub ShardProof<InnerSC>);
 
@@ -156,7 +160,44 @@ pub struct SP1ReducedProofData(pub ShardProof<InnerSC>);
 pub struct SP1PlonkBn254ProofData(pub PlonkBn254Proof);
 
 #[derive(Serialize, Deserialize, Clone)]
-pub struct SP1PlonkProofData(pub PlonkBn254Proof);
+pub struct SP1Groth16Bn254ProofData(pub Groth16Bn254Proof);
+
+#[derive(Serialize, Deserialize, Clone)]
+pub enum SP1Bn254ProofData {
+    Plonk(PlonkBn254Proof),
+    Groth16(Groth16Bn254Proof),
+}
+
+impl SP1Bn254ProofData {
+    pub fn get_proof_system(&self) -> ProofSystem {
+        match self {
+            SP1Bn254ProofData::Plonk(_) => ProofSystem::Plonk,
+            SP1Bn254ProofData::Groth16(_) => ProofSystem::Groth16,
+        }
+    }
+
+    pub fn get_raw_proof(&self) -> &str {
+        match self {
+            SP1Bn254ProofData::Plonk(proof) => &proof.raw_proof,
+            SP1Bn254ProofData::Groth16(proof) => &proof.raw_proof,
+        }
+    }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum ProofSystem {
+    Plonk,
+    Groth16,
+}
+
+impl ProofSystem {
+    pub fn as_str(&self) -> &'static str {
+        match self {
+            ProofSystem::Plonk => "Plonk",
+            ProofSystem::Groth16 => "Groth16",
+        }
+    }
+}
 
 /// An intermediate proof which proves the execution over a range of shards.
 #[derive(Serialize, Deserialize, Clone)]
diff --git a/crates/prover/src/verify.rs b/crates/prover/src/verify.rs
index c6ed6fc89b..a8d99ec2ad 100644
--- a/crates/prover/src/verify.rs
+++ b/crates/prover/src/verify.rs
@@ -8,7 +8,9 @@ use sp1_core_executor::subproof::SubproofVerifier;
 use sp1_core_machine::{cpu::MAX_CPU_LOG_DEGREE, io::SP1PublicValues};
 use sp1_primitives::consts::WORD_SIZE;
 use sp1_recursion_core::{air::RecursionPublicValues, stark::config::BabyBearPoseidon2Outer};
-use sp1_recursion_gnark_ffi::{PlonkBn254Proof, PlonkBn254Prover};
+use sp1_recursion_gnark_ffi::{
+    Groth16Bn254Proof, Groth16Bn254Prover, PlonkBn254Proof, PlonkBn254Prover,
+};
 use sp1_stark::{
     air::{PublicValues, POSEIDON_NUM_WORDS, PV_DIGEST_NUM_WORDS},
     baby_bear_poseidon2::BabyBearPoseidon2,
@@ -33,6 +35,18 @@ pub enum PlonkVerificationError {
     InvalidPublicValues,
 }
 
+#[derive(Error, Debug)]
+pub enum Groth16VerificationError {
+    #[error(
+        "the verifying key does not match the inner groth16 bn254 proof's committed verifying key"
+    )]
+    InvalidVerificationKey,
+    #[error(
+        "the public values in the sp1 proof do not match the public values in the inner groth16 bn254 proof"
+    )]
+    InvalidPublicValues,
+}
+
 impl<C: SP1ProverComponents> SP1Prover<C> {
     /// Verify a core proof by verifying the shards, verifying lookup bus, verifying that the
     /// shards are contiguous and complete.
@@ -389,6 +403,27 @@ impl<C: SP1ProverComponents> SP1Prover<C> {
 
         Ok(())
     }
+
+    /// Verifies a Groth16 proof using the circuit artifacts in the build directory.
+    pub fn verify_groth16_bn254(
+        &self,
+        proof: &Groth16Bn254Proof,
+        vk: &SP1VerifyingKey,
+        public_values: &SP1PublicValues,
+        build_dir: &Path,
+    ) -> Result<()> {
+        let prover = Groth16Bn254Prover::new();
+
+        let vkey_hash = BigUint::from_str(&proof.public_inputs[0])?;
+        let committed_values_digest = BigUint::from_str(&proof.public_inputs[1])?;
+
+        // Verify the proof with the corresponding public inputs.
+        prover.verify(proof, &vkey_hash, &committed_values_digest, build_dir);
+
+        verify_groth16_bn254_public_inputs(vk, public_values, &proof.public_inputs)?;
+
+        Ok(())
+    }
 }
 
 /// Verify the vk_hash and public_values_hash in the public inputs of the PlonkBn254Proof match the
@@ -414,6 +449,29 @@ pub fn verify_plonk_bn254_public_inputs(
     Ok(())
 }
 
+/// Verify the vk_hash and public_values_hash in the public inputs of the Groth16Bn254Proof match the
+/// expected values.
+pub fn verify_groth16_bn254_public_inputs(
+    vk: &SP1VerifyingKey,
+    public_values: &SP1PublicValues,
+    groth16_bn254_public_inputs: &[String],
+) -> Result<()> {
+    let expected_vk_hash = BigUint::from_str(&groth16_bn254_public_inputs[0])?;
+    let expected_public_values_hash = BigUint::from_str(&groth16_bn254_public_inputs[1])?;
+
+    let vk_hash = vk.hash_bn254().as_canonical_biguint();
+    if vk_hash != expected_vk_hash {
+        return Err(Groth16VerificationError::InvalidVerificationKey.into());
+    }
+
+    let public_values_hash = public_values.hash();
+    if public_values_hash != expected_public_values_hash {
+        return Err(Groth16VerificationError::InvalidPublicValues.into());
+    }
+
+    Ok(())
+}
+
 impl<C: SP1ProverComponents> SubproofVerifier for &SP1Prover<C> {
     fn verify_deferred_proof(
         &self,
diff --git a/crates/recursion/gnark-cli/src/main.rs b/crates/recursion/gnark-cli/src/main.rs
index 61f5e0602a..ef5c4d9fd6 100644
--- a/crates/recursion/gnark-cli/src/main.rs
+++ b/crates/recursion/gnark-cli/src/main.rs
@@ -1,8 +1,12 @@
 //! A simple CLI that wraps the gnark-ffi crate. This is called using Docker in gnark-ffi when the
 //! native feature is disabled.
 
-use sp1_recursion_gnark_ffi::ffi::{
-    build_plonk_bn254, prove_plonk_bn254, test_plonk_bn254, verify_plonk_bn254,
+use sp1_recursion_gnark_ffi::{
+    ffi::{
+        build_groth16_bn254, build_plonk_bn254, test_groth16_bn254, test_plonk_bn254,
+        verify_groth16_bn254, verify_plonk_bn254,
+    },
+    ProofBn254,
 };
 
 use clap::{Args, Parser, Subcommand};
@@ -17,18 +21,19 @@ struct Cli {
     command: Command,
 }
 
-#[allow(clippy::enum_variant_names)]
 #[derive(Debug, Subcommand)]
 enum Command {
-    BuildPlonk(BuildArgs),
-    ProvePlonk(ProveArgs),
-    VerifyPlonk(VerifyArgs),
-    TestPlonk(TestArgs),
+    Build(BuildArgs),
+    Prove(ProveArgs),
+    Verify(VerifyArgs),
+    Test(TestArgs),
 }
 
 #[derive(Debug, Args)]
 struct BuildArgs {
     data_dir: String,
+    #[arg(short, long)]
+    system: String,
 }
 
 #[derive(Debug, Args)]
@@ -36,6 +41,8 @@ struct ProveArgs {
     data_dir: String,
     witness_path: String,
     output_path: String,
+    #[arg(short, long)]
+    system: String,
 }
 
 #[derive(Debug, Args)]
@@ -45,34 +52,62 @@ struct VerifyArgs {
     vkey_hash: String,
     committed_values_digest: String,
     output_path: String,
+    #[arg(short, long)]
+    system: String,
 }
 
 #[derive(Debug, Args)]
 struct TestArgs {
     witness_json: String,
     constraints_json: String,
+    #[arg(short, long)]
+    system: String,
 }
 
 fn run_build(args: BuildArgs) {
-    build_plonk_bn254(&args.data_dir);
+    match args.system.as_str() {
+        "plonk" => build_plonk_bn254(&args.data_dir),
+        "groth16" => build_groth16_bn254(&args.data_dir),
+        _ => panic!("Unsupported system: {}", args.system),
+    }
 }
 
 fn run_prove(args: ProveArgs) {
-    let proof = prove_plonk_bn254(&args.data_dir, &args.witness_path);
+    let proof = match args.system.as_str() {
+        "plonk" => prove_plonk_bn254(&args.data_dir, &args.witness_path),
+        "groth16" => prove_groth16_bn254(&args.data_dir, &args.witness_path),
+        _ => panic!("Unsupported system: {}", args.system),
+    };
     let mut file = File::create(&args.output_path).unwrap();
     bincode::serialize_into(&mut file, &proof).unwrap();
 }
 
+fn prove_plonk_bn254(data_dir: &str, witness_path: &str) -> ProofBn254 {
+    ProofBn254::Plonk(sp1_recursion_gnark_ffi::ffi::prove_plonk_bn254(data_dir, witness_path))
+}
+
+fn prove_groth16_bn254(data_dir: &str, witness_path: &str) -> ProofBn254 {
+    ProofBn254::Groth16(sp1_recursion_gnark_ffi::ffi::prove_groth16_bn254(data_dir, witness_path))
+}
+
 fn run_verify(args: VerifyArgs) {
-    // For proof, we read the string from file since it can be large.
     let file = File::open(&args.proof_path).unwrap();
     let proof = read_to_string(file).unwrap();
-    let result = verify_plonk_bn254(
-        &args.data_dir,
-        proof.trim(),
-        &args.vkey_hash,
-        &args.committed_values_digest,
-    );
+    let result = match args.system.as_str() {
+        "plonk" => verify_plonk_bn254(
+            &args.data_dir,
+            proof.trim(),
+            &args.vkey_hash,
+            &args.committed_values_digest,
+        ),
+        "groth16" => verify_groth16_bn254(
+            &args.data_dir,
+            proof.trim(),
+            &args.vkey_hash,
+            &args.committed_values_digest,
+        ),
+        _ => panic!("Unsupported system: {}", args.system),
+    };
     let output = match result {
         Ok(_) => "OK".to_string(),
         Err(e) => e,
@@ -82,16 +117,20 @@ fn run_verify(args: VerifyArgs) {
 }
 
 fn run_test(args: TestArgs) {
-    test_plonk_bn254(&args.witness_json, &args.constraints_json);
+    match args.system.as_str() {
+        "plonk" => test_plonk_bn254(&args.witness_json, &args.constraints_json),
+        "groth16" => test_groth16_bn254(&args.witness_json, &args.constraints_json),
+        _ => panic!("Unsupported system: {}", args.system),
+    }
 }
 
 fn main() {
     let cli = Cli::parse();
 
     match cli.command {
-        Command::BuildPlonk(args) => run_build(args),
-        Command::ProvePlonk(args) => run_prove(args),
-        Command::VerifyPlonk(args) => run_verify(args),
-        Command::TestPlonk(args) => run_test(args),
+        Command::Build(args) => run_build(args),
+        Command::Prove(args) => run_prove(args),
+        Command::Verify(args) => run_verify(args),
+        Command::Test(args) => run_test(args),
     }
 }
diff --git a/crates/recursion/gnark-ffi/assets/SP1Verifier.txt b/crates/recursion/gnark-ffi/assets/SP1Verifier.txt
index 0c009135e3..a18f01b819 100644
--- a/crates/recursion/gnark-ffi/assets/SP1Verifier.txt
+++ b/crates/recursion/gnark-ffi/assets/SP1Verifier.txt
@@ -2,12 +2,12 @@
 pragma solidity ^0.8.20;
 
 import {ISP1Verifier, ISP1VerifierWithHash} from "../ISP1Verifier.sol";
-import {PlonkVerifier} from "./PlonkVerifier.sol";
+import {{PROOF_SYSTEM}Verifier} from "./{PROOF_SYSTEM}Verifier.sol";
 
 /// @title SP1 Verifier
 /// @author Succinct Labs
 /// @notice This contracts implements a solidity verifier for SP1.
-contract SP1Verifier is PlonkVerifier, ISP1VerifierWithHash {
+contract SP1Verifier is {PROOF_SYSTEM}Verifier, ISP1VerifierWithHash {
     /// @notice Thrown when the verifier selector from this proof does not match the one in this
     /// verifier. This indicates that this proof was sent to the wrong verifier.
     /// @param received The verifier selector from the first 4 bytes of the proof.
@@ -58,4 +58,4 @@ contract SP1Verifier is PlonkVerifier, ISP1VerifierWithHash {
             revert InvalidProof();
         }
     }
-}
+}
\ No newline at end of file
diff --git a/crates/recursion/gnark-ffi/go/.gitignore b/crates/recursion/gnark-ffi/go/.gitignore
index b4a997102a..a485d2ce97 100644
--- a/crates/recursion/gnark-ffi/go/.gitignore
+++ b/crates/recursion/gnark-ffi/go/.gitignore
@@ -1,5 +1,5 @@
 constraints.json
-witness.json
+*witness.json
 lib/libbabybear.a
 build/
 main
\ No newline at end of file
diff --git a/crates/recursion/gnark-ffi/go/main.go b/crates/recursion/gnark-ffi/go/main.go
index ed782400f2..2d7334b06b 100644
--- a/crates/recursion/gnark-ffi/go/main.go
+++ b/crates/recursion/gnark-ffi/go/main.go
@@ -8,6 +8,12 @@ typedef struct {
 	char *EncodedProof;
 	char *RawProof;
 } C_PlonkBn254Proof;
+
+typedef struct {
+	char *PublicInputs[2];
+	char *EncodedProof;
+	char *RawProof;
+} C_Groth16Bn254Proof;
 */
 import "C"
 import (
@@ -35,7 +41,7 @@ func ProvePlonkBn254(dataDir *C.char, witnessPath *C.char) *C.C_PlonkBn254Proof
 	dataDirString := C.GoString(dataDir)
 	witnessPathString := C.GoString(witnessPath)
 
-	sp1PlonkBn254Proof := sp1.Prove(dataDirString, witnessPathString)
+	sp1PlonkBn254Proof := sp1.ProvePlonk(dataDirString, witnessPathString)
 
 	ms := C.malloc(C.sizeof_C_PlonkBn254Proof)
 	if ms == nil {
@@ -55,7 +61,7 @@ func BuildPlonkBn254(dataDir *C.char) {
 	// Sanity check the required arguments have been provided.
 	dataDirString := C.GoString(dataDir)
 
-	sp1.Build(dataDirString)
+	sp1.BuildPlonk(dataDirString)
 }
 
 //export VerifyPlonkBn254
@@ -65,7 +71,7 @@ func VerifyPlonkBn254(dataDir *C.char, proof *C.char, vkeyHash *C.char, commited
 	vkeyHashString := C.GoString(vkeyHash)
 	commitedValuesDigestString := C.GoString(commitedValuesDigest)
 
-	err := sp1.Verify(dataDirString, proofString, vkeyHashString, commitedValuesDigestString)
+	err := sp1.VerifyPlonk(dataDirString, proofString, vkeyHashString, commitedValuesDigestString)
 	if err != nil {
 		return C.CString(err.Error())
 	}
@@ -90,11 +96,69 @@ func TestPlonkBn254(witnessPath *C.char, constraintsJson *C.char) *C.char {
 	return nil
 }
 
+//export ProveGroth16Bn254
+func ProveGroth16Bn254(dataDir *C.char, witnessPath *C.char) *C.C_Groth16Bn254Proof {
+	dataDirString := C.GoString(dataDir)
+	witnessPathString := C.GoString(witnessPath)
+
+	sp1Groth16Bn254Proof := sp1.ProveGroth16(dataDirString, witnessPathString)
+
+	ms := C.malloc(C.sizeof_C_Groth16Bn254Proof)
+	if ms == nil {
+		return nil
+	}
+
+	structPtr := (*C.C_Groth16Bn254Proof)(ms)
+	structPtr.PublicInputs[0] = C.CString(sp1Groth16Bn254Proof.PublicInputs[0])
+	structPtr.PublicInputs[1] = C.CString(sp1Groth16Bn254Proof.PublicInputs[1])
+	structPtr.EncodedProof = C.CString(sp1Groth16Bn254Proof.EncodedProof)
+	structPtr.RawProof = C.CString(sp1Groth16Bn254Proof.RawProof)
+	return structPtr
+}
+
+//export BuildGroth16Bn254
+func BuildGroth16Bn254(dataDir *C.char) {
+	// Sanity check the required arguments have been provided.
+	dataDirString := C.GoString(dataDir)
+
+	sp1.BuildGroth16(dataDirString)
+}
+
+//export VerifyGroth16Bn254
+func VerifyGroth16Bn254(dataDir *C.char, proof *C.char, vkeyHash *C.char, committedValuesDigest *C.char) *C.char {
+	dataDirString := C.GoString(dataDir)
+	proofString := C.GoString(proof)
+	vkeyHashString := C.GoString(vkeyHash)
+	committedValuesDigestString := C.GoString(committedValuesDigest)
+
+	err := sp1.VerifyGroth16(dataDirString, proofString, vkeyHashString, committedValuesDigestString)
+	if err != nil {
+		return C.CString(err.Error())
+	}
+	return nil
+}
+
+//export TestGroth16Bn254
+func TestGroth16Bn254(witnessJson *C.char, constraintsJson *C.char) *C.char {
+	// Because of the global env variables used here, we need to lock this function
+	testMutex.Lock()
+	witnessPathString := C.GoString(witnessJson)
+	constraintsJsonString := C.GoString(constraintsJson)
+	os.Setenv("WITNESS_JSON", witnessPathString)
+	os.Setenv("CONSTRAINTS_JSON", constraintsJsonString)
+	err := TestMain()
+	testMutex.Unlock()
+	if err != nil {
+		return C.CString(err.Error())
+	}
+	return nil
+}
+
 func TestMain() error {
 	// Get the file name from an environment variable.
 	fileName := os.Getenv("WITNESS_JSON")
 	if fileName == "" {
-		fileName = "witness.json"
+		fileName = "plonk_witness.json"
 	}
 
 	// Read the file.
diff --git a/crates/recursion/gnark-ffi/go/sp1/build.go b/crates/recursion/gnark-ffi/go/sp1/build.go
index 5531b7ebb5..36bc079850 100644
--- a/crates/recursion/gnark-ffi/go/sp1/build.go
+++ b/crates/recursion/gnark-ffi/go/sp1/build.go
@@ -9,14 +9,16 @@ import (
 
 	"github.com/consensys/gnark-crypto/ecc"
 	"github.com/consensys/gnark-crypto/kzg"
+	groth16 "github.com/consensys/gnark/backend/groth16"
 	"github.com/consensys/gnark/backend/plonk"
 	"github.com/consensys/gnark/frontend"
+	"github.com/consensys/gnark/frontend/cs/r1cs"
 	"github.com/consensys/gnark/frontend/cs/scs"
 	"github.com/consensys/gnark/test/unsafekzg"
 	"github.com/succinctlabs/sp1-recursion-gnark/sp1/trusted_setup"
 )
 
-func Build(dataDir string) {
+func BuildPlonk(dataDir string) {
 	// Set the enviroment variable for the constraints file.
 	//
 	// TODO: There might be some non-determinism if a single process is running this command
@@ -24,7 +26,7 @@ func Build(dataDir string) {
 	os.Setenv("CONSTRAINTS_JSON", dataDir+"/"+constraintsJsonFile)
 
 	// Read the file.
-	witnessInputPath := dataDir + "/witness.json"
+	witnessInputPath := dataDir + "/" + plonkWitnessPath
 	data, err := os.ReadFile(witnessInputPath)
 	if err != nil {
 		panic(err)
@@ -152,7 +154,7 @@ func Build(dataDir string) {
 	os.MkdirAll(dataDir, 0755)
 
 	// Write the solidity verifier.
-	solidityVerifierFile, err := os.Create(dataDir + "/" + verifierContractPath)
+	solidityVerifierFile, err := os.Create(dataDir + "/" + plonkVerifierContractPath)
 	if err != nil {
 		panic(err)
 	}
@@ -160,7 +162,7 @@ func Build(dataDir string) {
 	defer solidityVerifierFile.Close()
 
 	// Write the R1CS.
-	scsFile, err := os.Create(dataDir + "/" + circuitPath)
+	scsFile, err := os.Create(dataDir + "/" + plonkCircuitPath)
 	if err != nil {
 		panic(err)
 	}
@@ -171,7 +173,7 @@ func Build(dataDir string) {
 	}
 
 	// Write the verifier key.
-	vkFile, err := os.Create(dataDir + "/" + vkPath)
+	vkFile, err := os.Create(dataDir + "/" + plonkVkPath)
 	if err != nil {
 		panic(err)
 	}
@@ -182,7 +184,106 @@ func Build(dataDir string) {
 	}
 
 	// Write the proving key.
-	pkFile, err := os.Create(dataDir + "/" + pkPath)
+	pkFile, err := os.Create(dataDir + "/" + plonkPkPath)
+	if err != nil {
+		panic(err)
+	}
+	defer pkFile.Close()
+	_, err = pk.WriteTo(pkFile)
+	if err != nil {
+		panic(err)
+	}
+}
+
+func BuildGroth16(dataDir string) {
+	// Set the environment variable for the constraints file.
+	os.Setenv("CONSTRAINTS_JSON", dataDir+"/"+constraintsJsonFile)
+
+	// Read the file.
+	witnessInputPath := dataDir + "/" + groth16WitnessPath
+	data, err := os.ReadFile(witnessInputPath)
+	if err != nil {
+		panic(err)
+	}
+
+	// Deserialize the JSON data into a slice of Instruction structs
+	var witnessInput WitnessInput
+	err = json.Unmarshal(data, &witnessInput)
+	if err != nil {
+		panic(err)
+	}
+
+	// Initialize the circuit.
+	circuit := NewCircuit(witnessInput)
+
+	// Compile the circuit.
+	r1cs, err := frontend.Compile(ecc.BN254.ScalarField(), r1cs.NewBuilder, &circuit)
+	if err != nil {
+		panic(err)
+	}
+
+	// Generate the proving and verifying key.
+	pk, vk, err := groth16.Setup(r1cs)
+	if err != nil {
+		panic(err)
+	}
+
+	// Generate proof.
+	assignment := NewCircuit(witnessInput)
+	witness, err := frontend.NewWitness(&assignment, ecc.BN254.ScalarField())
+	if err != nil {
+		panic(err)
+	}
+	proof, err := groth16.Prove(r1cs, pk, witness)
+	if err != nil {
+		panic(err)
+	}
+
+	// Verify proof.
+	publicWitness, err := witness.Public()
+	if err != nil {
+		panic(err)
+	}
+	err = groth16.Verify(proof, vk, publicWitness)
+	if err != nil {
+		panic(err)
+	}
+
+	// Create the build directory.
+	os.MkdirAll(dataDir, 0755)
+
+	// Write the solidity verifier.
+	solidityVerifierFile, err := os.Create(dataDir + "/" + groth16VerifierContractPath)
+	if err != nil {
+		panic(err)
+	}
+	vk.ExportSolidity(solidityVerifierFile)
+	defer solidityVerifierFile.Close()
+
+	// Write the R1CS.
+	r1csFile, err := os.Create(dataDir + "/" + groth16CircuitPath)
+	if err != nil {
+		panic(err)
+	}
+	defer r1csFile.Close()
+	_, err = r1cs.WriteTo(r1csFile)
+	if err != nil {
+		panic(err)
+	}
+
+	// Write the verifier key.
+	vkFile, err := os.Create(dataDir + "/" + groth16VkPath)
+	if err != nil {
+		panic(err)
+	}
+	defer vkFile.Close()
+	_, err = vk.WriteTo(vkFile)
+	if err != nil {
+		panic(err)
+	}
+
+	// Write the proving key.
+	pkFile, err := os.Create(dataDir + "/" + groth16PkPath)
 	if err != nil {
 		panic(err)
 	}
diff --git a/crates/recursion/gnark-ffi/go/sp1/prove.go b/crates/recursion/gnark-ffi/go/sp1/prove.go
index 7260f99ff7..595bbef6cc 100644
--- a/crates/recursion/gnark-ffi/go/sp1/prove.go
+++ b/crates/recursion/gnark-ffi/go/sp1/prove.go
@@ -6,11 +6,12 @@ import (
 	"os"
 
 	"github.com/consensys/gnark-crypto/ecc"
+	"github.com/consensys/gnark/backend/groth16"
 	"github.com/consensys/gnark/backend/plonk"
 	"github.com/consensys/gnark/frontend"
 )
 
-func Prove(dataDir string, witnessPath string) Proof {
+func ProvePlonk(dataDir string, witnessPath string) Proof {
 	// Sanity check the required arguments have been provided.
 	if dataDir == "" {
 		panic("dataDirStr is required")
@@ -18,7 +19,7 @@ func Prove(dataDir string, witnessPath string) Proof {
 	os.Setenv("CONSTRAINTS_JSON", dataDir+"/"+constraintsJsonFile)
 
 	// Read the R1CS.
-	scsFile, err := os.Open(dataDir + "/" + circuitPath)
+	scsFile, err := os.Open(dataDir + "/" + plonkCircuitPath)
 	if err != nil {
 		panic(err)
 	}
@@ -27,7 +28,7 @@ func Prove(dataDir string, witnessPath string) Proof {
 	defer scsFile.Close()
 
 	// Read the proving key.
-	pkFile, err := os.Open(dataDir + "/" + pkPath)
+	pkFile, err := os.Open(dataDir + "/" + plonkPkPath)
 	if err != nil {
 		panic(err)
 	}
@@ -37,7 +38,7 @@ func Prove(dataDir string, witnessPath string) Proof {
 	defer pkFile.Close()
 
 	// Read the verifier key.
-	vkFile, err := os.Open(dataDir + "/" + vkPath)
+	vkFile, err := os.Open(dataDir + "/" + plonkVkPath)
 	if err != nil {
 		panic(err)
 	}
@@ -83,3 +84,77 @@ func Prove(dataDir string, witnessPath string) Proof {
 
 	return NewSP1PlonkBn254Proof(&proof, witnessInput)
 }
+
+func ProveGroth16(dataDir string, witnessPath string) Proof {
+	// Sanity check the required arguments have been provided.
+	if dataDir == "" {
+		panic("dataDirStr is required")
+	}
+	os.Setenv("CONSTRAINTS_JSON", dataDir+"/"+constraintsJsonFile)
+
+	// Read the R1CS.
+	r1csFile, err := os.Open(dataDir + "/" + groth16CircuitPath)
+	if err != nil {
+		panic(err)
+	}
+	r1cs := groth16.NewCS(ecc.BN254)
+	r1cs.ReadFrom(r1csFile)
+	defer r1csFile.Close()
+
+	// Read the proving key.
+	pkFile, err := os.Open(dataDir + "/" + groth16PkPath)
+	if err != nil {
+		panic(err)
+	}
+	pk := groth16.NewProvingKey(ecc.BN254)
+	bufReader := bufio.NewReaderSize(pkFile, 1024*1024)
+	pk.UnsafeReadFrom(bufReader)
+	defer pkFile.Close()
+
+	// Read the verifier key.
+	vkFile, err := os.Open(dataDir + "/" + groth16VkPath)
+	if err != nil {
+		panic(err)
+	}
+	vk := groth16.NewVerifyingKey(ecc.BN254)
+	vk.ReadFrom(vkFile)
+	defer vkFile.Close()
+
+	// Read the file.
+	data, err := os.ReadFile(witnessPath)
+	if err != nil {
+		panic(err)
+	}
+
+	// Deserialize the JSON data into a slice of Instruction structs
+	var witnessInput WitnessInput
+	err = json.Unmarshal(data, &witnessInput)
+	if err != nil {
+		panic(err)
+	}
+
+	// Generate the witness.
+	assignment := NewCircuit(witnessInput)
+	witness, err := frontend.NewWitness(&assignment, ecc.BN254.ScalarField())
+	if err != nil {
+		panic(err)
+	}
+	publicWitness, err := witness.Public()
+	if err != nil {
+		panic(err)
+	}
+
+	// Generate the proof.
+	proof, err := groth16.Prove(r1cs, pk, witness)
+	if err != nil {
+		panic(err)
+	}
+
+	// Verify proof.
+	err = groth16.Verify(proof, vk, publicWitness)
+	if err != nil {
+		panic(err)
+	}
+
+	return NewSP1Groth16Proof(&proof, witnessInput)
+}
diff --git a/crates/recursion/gnark-ffi/go/sp1/sp1.go b/crates/recursion/gnark-ffi/go/sp1/sp1.go
index 5760a43d90..eebef363b1 100644
--- a/crates/recursion/gnark-ffi/go/sp1/sp1.go
+++ b/crates/recursion/gnark-ffi/go/sp1/sp1.go
@@ -14,10 +14,16 @@ import (
 var srsFile string = "srs.bin"
 var srsLagrangeFile string = "srs_lagrange.bin"
 var constraintsJsonFile string = "constraints.json"
-var verifierContractPath string = "PlonkVerifier.sol"
-var circuitPath string = "circuit.bin"
-var vkPath string = "vk.bin"
-var pkPath string = "pk.bin"
+var plonkVerifierContractPath string = "PlonkVerifier.sol"
+var groth16VerifierContractPath string = "Groth16Verifier.sol"
+var plonkCircuitPath string = "plonk_circuit.bin"
+var groth16CircuitPath string = "groth16_circuit.bin"
+var plonkVkPath string = "plonk_vk.bin"
+var groth16VkPath string = "groth16_vk.bin"
+var plonkPkPath string = "plonk_pk.bin"
+var groth16PkPath string = "groth16_pk.bin"
+var plonkWitnessPath string = "plonk_witness.json"
+var groth16WitnessPath string = "groth16_witness.json"
 
 type Circuit struct {
 	VkeyHash             frontend.Variable `gnark:",public"`
diff --git a/crates/recursion/gnark-ffi/go/sp1/utils.go b/crates/recursion/gnark-ffi/go/sp1/utils.go
index f4711d9c61..6456c90b2a 100644
--- a/crates/recursion/gnark-ffi/go/sp1/utils.go
+++ b/crates/recursion/gnark-ffi/go/sp1/utils.go
@@ -4,6 +4,8 @@ import (
 	"bytes"
 	"encoding/hex"
 
+	groth16 "github.com/consensys/gnark/backend/groth16"
+	groth16_bn254 "github.com/consensys/gnark/backend/groth16/bn254"
 	plonk "github.com/consensys/gnark/backend/plonk"
 	plonk_bn254 "github.com/consensys/gnark/backend/plonk/bn254"
 	"github.com/consensys/gnark/frontend"
@@ -31,6 +33,27 @@ func NewSP1PlonkBn254Proof(proof *plonk.Proof, witnessInput WitnessInput) Proof
 	}
 }
 
+func NewSP1Groth16Proof(proof *groth16.Proof, witnessInput WitnessInput) Proof {
+	var buf bytes.Buffer
+	(*proof).WriteRawTo(&buf)
+	proofBytes := buf.Bytes()
+
+	var publicInputs [2]string
+	publicInputs[0] = witnessInput.VkeyHash
+	publicInputs[1] = witnessInput.CommitedValuesDigest
+
+	// Cast groth16 proof into groth16_bn254 proof so we can call MarshalSolidity.
+	p := (*proof).(*groth16_bn254.Proof)
+
+	encodedProof := p.MarshalSolidity()
+
+	return Proof{
+		PublicInputs: publicInputs,
+		EncodedProof: hex.EncodeToString(encodedProof),
+		RawProof:     hex.EncodeToString(proofBytes),
+	}
+}
+
 func NewCircuit(witnessInput WitnessInput) Circuit {
 	vars := make([]frontend.Variable, len(witnessInput.Vars))
 	felts := make([]babybear.Variable, len(witnessInput.Felts))
diff --git a/crates/recursion/gnark-ffi/go/sp1/verify.go b/crates/recursion/gnark-ffi/go/sp1/verify.go
index 27c459c991..273e854d11 100644
--- a/crates/recursion/gnark-ffi/go/sp1/verify.go
+++ b/crates/recursion/gnark-ffi/go/sp1/verify.go
@@ -6,12 +6,13 @@ import (
 	"os"
 
 	"github.com/consensys/gnark-crypto/ecc"
+	"github.com/consensys/gnark/backend/groth16"
 	"github.com/consensys/gnark/backend/plonk"
 	"github.com/consensys/gnark/frontend"
 	"github.com/succinctlabs/sp1-recursion-gnark/sp1/babybear"
 )
 
-func Verify(verifyCmdDataDir string, verifyCmdProof string, verifyCmdVkeyHash string, verifyCmdCommitedValuesDigest string) error {
+func VerifyPlonk(verifyCmdDataDir string, verifyCmdProof string, verifyCmdVkeyHash string, verifyCmdCommitedValuesDigest string) error {
 	// Sanity check the required arguments have been provided.
 	if verifyCmdDataDir == "" {
 		panic("--data is required")
@@ -28,7 +29,7 @@ func Verify(verifyCmdDataDir string, verifyCmdProof string, verifyCmdVkeyHash st
 	}
 
 	// Read the verifier key.
-	vkFile, err := os.Open(verifyCmdDataDir + "/" + vkPath)
+	vkFile, err := os.Open(verifyCmdDataDir + "/" + plonkVkPath)
 	if err != nil {
 		panic(err)
 	}
@@ -56,3 +57,49 @@ func Verify(verifyCmdDataDir string, verifyCmdProof string, verifyCmdVkeyHash st
 	err = plonk.Verify(proof, vk, publicWitness)
 	return err
 }
+
+func VerifyGroth16(verifyCmdDataDir string, verifyCmdProof string, verifyCmdVkeyHash string, verifyCmdCommitedValuesDigest string) error {
+	// Sanity check the required arguments have been provided.
+	if verifyCmdDataDir == "" {
+		panic("--data is required")
+	}
+
+	// Decode the proof.
+	proofDecodedBytes, err := hex.DecodeString(verifyCmdProof)
+	if err != nil {
+		panic(err)
+	}
+	proof := groth16.NewProof(ecc.BN254)
+	if _, err := proof.ReadFrom(bytes.NewReader(proofDecodedBytes)); err != nil {
+		panic(err)
+	}
+
+	// Read the verifier key.
+	vkFile, err := os.Open(verifyCmdDataDir + "/" + groth16VkPath)
+	if err != nil {
+		panic(err)
+	}
+	vk := groth16.NewVerifyingKey(ecc.BN254)
+	vk.ReadFrom(vkFile)
+
+	// Compute the public witness.
+	circuit := Circuit{
+		Vars:                 []frontend.Variable{},
+		Felts:                []babybear.Variable{},
+		Exts:                 []babybear.ExtensionVariable{},
+		VkeyHash:             verifyCmdVkeyHash,
+		CommitedValuesDigest: verifyCmdCommitedValuesDigest,
+	}
+	witness, err := frontend.NewWitness(&circuit, ecc.BN254.ScalarField())
+	if err != nil {
+		panic(err)
+	}
+	publicWitness, err := witness.Public()
+	if err != nil {
+		panic(err)
+	}
+
+	// Verify proof.
+	err = groth16.Verify(proof, vk, publicWitness)
+	return err
+}
diff --git a/crates/recursion/gnark-ffi/src/ffi/docker.rs b/crates/recursion/gnark-ffi/src/ffi/docker.rs
index f7c5e243dd..9941afb7b4 100644
--- a/crates/recursion/gnark-ffi/src/ffi/docker.rs
+++ b/crates/recursion/gnark-ffi/src/ffi/docker.rs
@@ -1,8 +1,23 @@
+use crate::{Groth16Bn254Proof, PlonkBn254Proof};
+use anyhow::{anyhow, Result};
 use sp1_core_machine::SP1_CIRCUIT_VERSION;
-
-use crate::PlonkBn254Proof;
 use std::{io::Write, process::Command};
 
+/// Represents the proof system being used
+enum ProofSystem {
+    Plonk,
+    Groth16,
+}
+
+impl ProofSystem {
+    fn as_str(&self) -> &'static str {
+        match self {
+            ProofSystem::Plonk => "plonk",
+            ProofSystem::Groth16 => "groth16",
+        }
+    }
+}
+
 /// Checks that docker is installed and running.
 fn check_docker() -> bool {
     let output = Command::new("docker").arg("info").output();
@@ -22,7 +37,7 @@ fn get_docker_image() -> String {
 }
 
 /// Calls `docker run` with the given arguments and bind mounts.
-fn call_docker(args: &[&str], mounts: &[(&str, &str)]) -> anyhow::Result<()> {
+fn call_docker(args: &[&str], mounts: &[(&str, &str)]) -> Result<()> {
     log::info!("Running {} in docker", args[0]);
     let mut cmd = Command::new("docker");
     cmd.args(["run", "--rm"]);
@@ -33,41 +48,63 @@ fn call_docker(args: &[&str], mounts: &[(&str, &str)]) -> anyhow::Result<()> {
     cmd.args(args);
     if !cmd.status()?.success() {
         log::error!("Failed to run `docker run`: {:?}", cmd);
-        return Err(anyhow::anyhow!("docker command failed"));
+        return Err(anyhow!("docker command failed"));
     }
     Ok(())
 }
 
-pub fn prove_plonk_bn254(data_dir: &str, witness_path: &str) -> PlonkBn254Proof {
-    let output_file = tempfile::NamedTempFile::new().unwrap();
+fn prove(system: ProofSystem, data_dir: &str, witness_path: &str) -> Result<Vec<u8>> {
+    let output_file = tempfile::NamedTempFile::new()?;
     let mounts = [
         (data_dir, "/circuit"),
         (witness_path, "/witness"),
         (output_file.path().to_str().unwrap(), "/output"),
     ];
     assert_docker();
-    call_docker(&["prove-plonk", "/circuit", "/witness", "/output"], &mounts)
-        .expect("failed to prove with docker");
-    bincode::deserialize_from(&output_file).expect("failed to deserialize result")
+    call_docker(
+        &["prove", "--system", system.as_str(), "/circuit", "/witness", "/output"],
+        &mounts,
+    )?;
+    Ok(std::fs::read(output_file.path())?)
 }
 
-pub fn build_plonk_bn254(data_dir: &str) {
+pub fn prove_plonk_bn254(data_dir: &str, witness_path: &str) -> PlonkBn254Proof {
+    let result =
+        prove(ProofSystem::Plonk, data_dir, witness_path).expect("failed to prove with docker");
+    bincode::deserialize(&result).expect("failed to deserialize result")
+}
+
+pub fn prove_groth16_bn254(data_dir: &str, witness_path: &str) -> Groth16Bn254Proof {
+    let result =
+        prove(ProofSystem::Groth16, data_dir, witness_path).expect("failed to prove with docker");
+    bincode::deserialize(&result).expect("failed to deserialize result")
+}
+
+fn build(system: ProofSystem, data_dir: &str) -> Result<()> {
     let circuit_dir = if data_dir.ends_with("dev") { "/circuit_dev" } else { "/circuit" };
     let mounts = [(data_dir, circuit_dir)];
     assert_docker();
-    call_docker(&["build-plonk", circuit_dir], &mounts).expect("failed to build with docker");
+    call_docker(&["build", "--system", system.as_str(), circuit_dir], &mounts)
 }
 
-pub fn verify_plonk_bn254(
+pub fn build_plonk_bn254(data_dir: &str) {
+    build(ProofSystem::Plonk, data_dir).expect("failed to build with docker");
+}
+
+pub fn build_groth16_bn254(data_dir: &str) {
+    build(ProofSystem::Groth16, data_dir).expect("failed to build with docker");
+}
+
+fn verify(
+    system: ProofSystem,
     data_dir: &str,
     proof: &str,
     vkey_hash: &str,
     committed_values_digest: &str,
-) -> Result<(), String> {
-    // Write proof string to a file since it can be large.
-    let mut proof_file = tempfile::NamedTempFile::new().unwrap();
-    proof_file.write_all(proof.as_bytes()).unwrap();
-    let output_file = tempfile::NamedTempFile::new().unwrap();
+) -> Result<()> {
+    let mut proof_file = tempfile::NamedTempFile::new()?;
+    proof_file.write_all(proof.as_bytes())?;
+    let output_file = tempfile::NamedTempFile::new()?;
     let mounts = [
         (data_dir, "/circuit"),
         (proof_file.path().to_str().unwrap(), "/proof"),
@@ -75,23 +112,56 @@ pub fn verify_plonk_bn254(
     ];
     assert_docker();
     call_docker(
-        &["verify-plonk", "/circuit", "/proof", vkey_hash, committed_values_digest, "/output"],
+        &[
+            "verify",
+            "--system",
+            system.as_str(),
+            "/circuit",
+            "/proof",
+            vkey_hash,
+            committed_values_digest,
+            "/output",
+        ],
         &mounts,
-    )
-    .expect("failed to verify with docker");
-    let result = std::fs::read_to_string(output_file.path()).unwrap();
+    )?;
+    let result = std::fs::read_to_string(output_file.path())?;
     if result == "OK" {
         Ok(())
     } else {
-        Err(result)
+        Err(anyhow!(result))
     }
 }
 
-pub fn test_plonk_bn254(witness_json: &str, constraints_json: &str) {
+pub fn verify_plonk_bn254(
+    data_dir: &str,
+    proof: &str,
+    vkey_hash: &str,
+    committed_values_digest: &str,
+) -> Result<()> {
+    verify(ProofSystem::Plonk, data_dir, proof, vkey_hash, committed_values_digest)
+}
+
+pub fn verify_groth16_bn254(
+    data_dir: &str,
+    proof: &str,
+    vkey_hash: &str,
+    committed_values_digest: &str,
+) -> Result<()> {
+    verify(ProofSystem::Groth16, data_dir, proof, vkey_hash, committed_values_digest)
+}
+
+fn test(system: ProofSystem, witness_json: &str, constraints_json: &str) -> Result<()> {
     let mounts = [(constraints_json, "/constraints"), (witness_json, "/witness")];
     assert_docker();
-    call_docker(&["test-plonk", "/constraints", "/witness"], &mounts)
-        .expect("failed to test with docker");
+    call_docker(&["test", "--system", system.as_str(), "/constraints", "/witness"], &mounts)
+}
+
+pub fn test_plonk_bn254(witness_json: &str, constraints_json: &str) {
+    test(ProofSystem::Plonk, witness_json, constraints_json).expect("failed to test with docker");
+}
+
+pub fn test_groth16_bn254(witness_json: &str, constraints_json: &str) {
+    test(ProofSystem::Groth16, witness_json, constraints_json).expect("failed to test with docker");
 }
 
 pub fn test_babybear_poseidon2() {
diff --git a/crates/recursion/gnark-ffi/src/ffi/native.rs b/crates/recursion/gnark-ffi/src/ffi/native.rs
index 9a197eb1bc..d3faba0fe8 100644
--- a/crates/recursion/gnark-ffi/src/ffi/native.rs
+++ b/crates/recursion/gnark-ffi/src/ffi/native.rs
@@ -5,7 +5,7 @@
 //! Although we cast to *mut c_char because the Go signatures can't be immutable, the Go functions
 //! should not modify the strings.
 
-use crate::PlonkBn254Proof;
+use crate::{Groth16Bn254Proof, PlonkBn254Proof};
 use cfg_if::cfg_if;
 use sp1_core_machine::SP1_CIRCUIT_VERSION;
 use std::ffi::{c_char, CString};
@@ -16,31 +16,83 @@ mod bind {
 }
 use bind::*;
 
-pub fn prove_plonk_bn254(data_dir: &str, witness_path: &str) -> PlonkBn254Proof {
-    let data_dir = CString::new(data_dir).expect("CString::new failed");
-    let witness_path = CString::new(witness_path).expect("CString::new failed");
+enum ProofSystem {
+    Plonk,
+    Groth16,
+}
 
-    let proof = unsafe {
-        let proof = bind::ProvePlonkBn254(
-            data_dir.as_ptr() as *mut c_char,
-            witness_path.as_ptr() as *mut c_char,
-        );
-        // Safety: The pointer is returned from the go code and is guaranteed to be valid.
-        *proof
-    };
+enum ProofResult {
+    Plonk(C_PlonkBn254Proof),
+    Groth16(C_Groth16Bn254Proof),
+}
+
+impl ProofSystem {
+    fn build_fn(&self) -> unsafe extern "C" fn(*mut c_char) {
+        match self {
+            ProofSystem::Plonk => bind::BuildPlonkBn254,
+            ProofSystem::Groth16 => bind::BuildGroth16Bn254,
+        }
+    }
+
+    fn prove_fn(&self) -> ProveFunction {
+        match self {
+            ProofSystem::Plonk => ProveFunction::Plonk(bind::ProvePlonkBn254),
+            ProofSystem::Groth16 => ProveFunction::Groth16(bind::ProveGroth16Bn254),
+        }
+    }
+
+    fn verify_fn(
+        &self,
+    ) -> unsafe extern "C" fn(*mut c_char, *mut c_char, *mut c_char, *mut c_char) -> *mut c_char
+    {
+        match self {
+            ProofSystem::Plonk => bind::VerifyPlonkBn254,
+            ProofSystem::Groth16 => bind::VerifyGroth16Bn254,
+        }
+    }
 
-    proof.into_rust()
+    fn test_fn(&self) -> unsafe extern "C" fn(*mut c_char, *mut c_char) -> *mut c_char {
+        match self {
+            ProofSystem::Plonk => bind::TestPlonkBn254,
+            ProofSystem::Groth16 => bind::TestGroth16Bn254,
+        }
+    }
 }
 
-pub fn build_plonk_bn254(data_dir: &str) {
+enum ProveFunction {
+    Plonk(unsafe extern "C" fn(*mut c_char, *mut c_char) -> *mut C_PlonkBn254Proof),
+    Groth16(unsafe extern "C" fn(*mut c_char, *mut c_char) -> *mut C_Groth16Bn254Proof),
+}
+
+fn build(system: ProofSystem, data_dir: &str) {
+    let data_dir = CString::new(data_dir).expect("CString::new failed");
+    unsafe {
+        (system.build_fn())(data_dir.as_ptr() as *mut c_char);
+    }
+}
+
+fn prove(system: ProofSystem, data_dir: &str, witness_path: &str) -> ProofResult {
     let data_dir = CString::new(data_dir).expect("CString::new failed");
+    let witness_path = CString::new(witness_path).expect("CString::new failed");
 
     unsafe {
-        bind::BuildPlonkBn254(data_dir.as_ptr() as *mut c_char);
+        match system.prove_fn() {
+            ProveFunction::Plonk(func) => {
+                let proof =
+                    func(data_dir.as_ptr() as *mut c_char, witness_path.as_ptr() as *mut c_char);
+                ProofResult::Plonk(*proof)
+            }
+            ProveFunction::Groth16(func) => {
+                let proof =
+                    func(data_dir.as_ptr() as *mut c_char, witness_path.as_ptr() as *mut c_char);
+                ProofResult::Groth16(*proof)
+            }
+        }
     }
 }
 
-pub fn verify_plonk_bn254(
+fn verify(
+    system: ProofSystem,
     data_dir: &str,
     proof: &str,
     vkey_hash: &str,
@@ -53,7 +105,7 @@ pub fn verify_plonk_bn254(
         CString::new(committed_values_digest).expect("CString::new failed");
 
     let err_ptr = unsafe {
-        bind::VerifyPlonkBn254(
+        (system.verify_fn())(
             data_dir.as_ptr() as *mut c_char,
             proof.as_ptr() as *mut c_char,
             vkey_hash.as_ptr() as *mut c_char,
@@ -69,29 +121,79 @@ pub fn verify_plonk_bn254(
     }
 }
 
-pub fn test_plonk_bn254(witness_json: &str, constraints_json: &str) {
+fn test(system: ProofSystem, witness_json: &str, constraints_json: &str) {
     unsafe {
         let witness_json = CString::new(witness_json).expect("CString::new failed");
-        let build_dir = CString::new(constraints_json).expect("CString::new failed");
-        let err_ptr = bind::TestPlonkBn254(
+        let constraints_json = CString::new(constraints_json).expect("CString::new failed");
+        let err_ptr = (system.test_fn())(
             witness_json.as_ptr() as *mut c_char,
-            build_dir.as_ptr() as *mut c_char,
+            constraints_json.as_ptr() as *mut c_char,
         );
         if !err_ptr.is_null() {
             // Safety: The error message is returned from the go code and is guaranteed to be valid.
             let err = CString::from_raw(err_ptr);
-            panic!("TestPlonkBn254 failed: {}", err.into_string().unwrap());
+            panic!("Test failed: {}", err.into_string().unwrap());
         }
     }
 }
 
+// Public API functions
+
+pub fn build_plonk_bn254(data_dir: &str) {
+    build(ProofSystem::Plonk, data_dir)
+}
+
+pub fn prove_plonk_bn254(data_dir: &str, witness_path: &str) -> PlonkBn254Proof {
+    match prove(ProofSystem::Plonk, data_dir, witness_path) {
+        ProofResult::Plonk(proof) => proof.into(),
+        _ => unreachable!(),
+    }
+}
+
+pub fn verify_plonk_bn254(
+    data_dir: &str,
+    proof: &str,
+    vkey_hash: &str,
+    committed_values_digest: &str,
+) -> Result<(), String> {
+    verify(ProofSystem::Plonk, data_dir, proof, vkey_hash, committed_values_digest)
+}
+
+pub fn test_plonk_bn254(witness_json: &str, constraints_json: &str) {
+    test(ProofSystem::Plonk, witness_json, constraints_json)
+}
+
+pub fn build_groth16_bn254(data_dir: &str) {
+    build(ProofSystem::Groth16, data_dir)
+}
+
+pub fn prove_groth16_bn254(data_dir: &str, witness_path: &str) -> Groth16Bn254Proof {
+    match prove(ProofSystem::Groth16, data_dir, witness_path) {
+        ProofResult::Groth16(proof) => proof.into(),
+        _ => unreachable!(),
+    }
+}
+
+pub fn verify_groth16_bn254(
+    data_dir: &str,
+    proof: &str,
+    vkey_hash: &str,
+    committed_values_digest: &str,
+) -> Result<(), String> {
+    verify(ProofSystem::Groth16, data_dir, proof, vkey_hash, committed_values_digest)
+}
+
+pub fn test_groth16_bn254(witness_json: &str, constraints_json: &str) {
+    test(ProofSystem::Groth16, witness_json, constraints_json)
+}
+
 pub fn test_babybear_poseidon2() {
     unsafe {
         let err_ptr = bind::TestPoseidonBabyBear2();
         if !err_ptr.is_null() {
             // Safety: The error message is returned from the go code and is guaranteed to be valid.
             let err = CString::from_raw(err_ptr);
-            panic!("TestPlonkBn254 failed: {}", err.into_string().unwrap());
+            panic!("TestPoseidonBabyBear2 failed: {}", err.into_string().unwrap());
         }
     }
 }
@@ -102,31 +204,43 @@ pub fn test_babybear_poseidon2() {
 /// This function frees the string memory, so the caller must ensure that the pointer is not used
 /// after this function is called.
 unsafe fn c_char_ptr_to_string(input: *mut c_char) -> String {
-    unsafe {
-        CString::from_raw(input) // Converts a pointer that C uses into a CString
-            .into_string()
-            .expect("CString::into_string failed")
-    }
+    CString::from_raw(input).into_string().expect("CString::into_string failed")
 }
 
-impl C_PlonkBn254Proof {
-    /// Converts a C PlonkBn254Proof into a Rust PlonkBn254Proof, freeing the C strings.
-    fn into_rust(self) -> PlonkBn254Proof {
+impl From<C_PlonkBn254Proof> for PlonkBn254Proof {
+    fn from(c_proof: C_PlonkBn254Proof) -> Self {
         // Safety: The raw pointers are not used anymore after converted into Rust strings.
         unsafe {
             PlonkBn254Proof {
                 public_inputs: [
-                    c_char_ptr_to_string(self.PublicInputs[0]),
-                    c_char_ptr_to_string(self.PublicInputs[1]),
+                    c_char_ptr_to_string(c_proof.PublicInputs[0]),
+                    c_char_ptr_to_string(c_proof.PublicInputs[1]),
                 ],
-                encoded_proof: c_char_ptr_to_string(self.EncodedProof),
-                raw_proof: c_char_ptr_to_string(self.RawProof),
+                encoded_proof: c_char_ptr_to_string(c_proof.EncodedProof),
+                raw_proof: c_char_ptr_to_string(c_proof.RawProof),
                 plonk_vkey_hash: [0; 32],
             }
         }
     }
 }
 
+impl From<C_Groth16Bn254Proof> for Groth16Bn254Proof {
+    fn from(c_proof: C_Groth16Bn254Proof) -> Self {
+        // Safety: The raw pointers are not used anymore after converted into Rust strings.
+        unsafe {
+            Groth16Bn254Proof {
+                public_inputs: [
+                    c_char_ptr_to_string(c_proof.PublicInputs[0]),
+                    c_char_ptr_to_string(c_proof.PublicInputs[1]),
+                ],
+                encoded_proof: c_char_ptr_to_string(c_proof.EncodedProof),
+                raw_proof: c_char_ptr_to_string(c_proof.RawProof),
+                groth16_vkey_hash: [0; 32],
+            }
+        }
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use p3_baby_bear::BabyBear;
diff --git a/crates/recursion/gnark-ffi/src/groth16_bn254.rs b/crates/recursion/gnark-ffi/src/groth16_bn254.rs
new file mode 100644
index 0000000000..a93f29badf
--- /dev/null
+++ b/crates/recursion/gnark-ffi/src/groth16_bn254.rs
@@ -0,0 +1,126 @@
+use std::{
+    fs::File,
+    io::Write,
+    path::{Path, PathBuf},
+};
+
+use crate::{
+    ffi::{build_groth16_bn254, prove_groth16_bn254, test_groth16_bn254, verify_groth16_bn254},
+    witness::GnarkWitness,
+    Groth16Bn254Proof,
+};
+
+use num_bigint::BigUint;
+use sha2::{Digest, Sha256};
+use sp1_core_machine::SP1_CIRCUIT_VERSION;
+use sp1_recursion_compiler::{
+    constraints::Constraint,
+    ir::{Config, Witness},
+};
+
+/// A prover that can generate proofs with the PLONK protocol using bindings to Gnark.
+#[derive(Debug, Clone)]
+pub struct Groth16Bn254Prover;
+
+/// A prover that can generate proofs with the Groth16 protocol using bindings to Gnark.
+impl Groth16Bn254Prover {
+    /// Creates a new [Groth16Bn254Prover].
+    pub fn new() -> Self {
+        Self
+    }
+
+    pub fn get_vkey_hash(build_dir: &Path) -> [u8; 32] {
+        let vkey_path = build_dir.join("groth16_vk.bin");
+        let vk_bin_bytes = std::fs::read(vkey_path).unwrap();
+        Sha256::digest(vk_bin_bytes).into()
+    }
+
+    /// Executes the prover in testing mode with a circuit definition and witness.
+    pub fn test<C: Config>(constraints: Vec<Constraint>, witness: Witness<C>) {
+        let serialized = serde_json::to_string(&constraints).unwrap();
+
+        // Write constraints.
+        let mut constraints_file = tempfile::NamedTempFile::new().unwrap();
+        constraints_file.write_all(serialized.as_bytes()).unwrap();
+
+        // Write witness.
+        let mut witness_file = tempfile::NamedTempFile::new().unwrap();
+        let gnark_witness = GnarkWitness::new(witness);
+        let serialized = serde_json::to_string(&gnark_witness).unwrap();
+        witness_file.write_all(serialized.as_bytes()).unwrap();
+
+        test_groth16_bn254(
+            witness_file.path().to_str().unwrap(),
+            constraints_file.path().to_str().unwrap(),
+        )
+    }
+
+    /// Builds the Groth16 circuit locally.
+    pub fn build<C: Config>(constraints: Vec<Constraint>, witness: Witness<C>, build_dir: PathBuf) {
+        let serialized = serde_json::to_string(&constraints).unwrap();
+
+        // Write constraints.
+        let constraints_path = build_dir.join("constraints.json");
+        let mut file = File::create(constraints_path).unwrap();
+        file.write_all(serialized.as_bytes()).unwrap();
+
+        // Write witness.
+        let witness_path = build_dir.join("groth16_witness.json");
+        let gnark_witness = GnarkWitness::new(witness);
+        let mut file = File::create(witness_path).unwrap();
+        let serialized = serde_json::to_string(&gnark_witness).unwrap();
+        file.write_all(serialized.as_bytes()).unwrap();
+
+        build_groth16_bn254(build_dir.to_str().unwrap());
+
+        // Write the corresponding asset files to the build dir.
+        let sp1_verifier_path = build_dir.join("Groth16SP1Verifier.sol");
+        let vkey_hash = Self::get_vkey_hash(&build_dir);
+        let sp1_verifier_str = include_str!("../assets/SP1Verifier.txt")
+            .replace("{SP1_CIRCUIT_VERSION}", SP1_CIRCUIT_VERSION)
+            .replace("{VERIFIER_HASH}", format!("0x{}", hex::encode(vkey_hash)).as_str())
+            .replace("{PROOF_SYSTEM}", "Groth16");
+        let mut sp1_verifier_file = File::create(sp1_verifier_path).unwrap();
+        sp1_verifier_file.write_all(sp1_verifier_str.as_bytes()).unwrap();
+    }
+
+    /// Generates a Groth16 proof given a witness.
+    pub fn prove<C: Config>(&self, witness: Witness<C>, build_dir: PathBuf) -> Groth16Bn254Proof {
+        // Write witness.
+        let mut witness_file = tempfile::NamedTempFile::new().unwrap();
+        let gnark_witness = GnarkWitness::new(witness);
+        let serialized = serde_json::to_string(&gnark_witness).unwrap();
+        witness_file.write_all(serialized.as_bytes()).unwrap();
+
+        let mut proof =
+            prove_groth16_bn254(build_dir.to_str().unwrap(), witness_file.path().to_str().unwrap());
+        proof.groth16_vkey_hash = Self::get_vkey_hash(&build_dir);
+        proof
+    }
+
+    /// Verify a Groth16proof and verify that the supplied vkey_hash and committed_values_digest match.
+    pub fn verify(
+        &self,
+        proof: &Groth16Bn254Proof,
+        vkey_hash: &BigUint,
+        committed_values_digest: &BigUint,
+        build_dir: &Path,
+    ) {
+        if proof.groth16_vkey_hash != Self::get_vkey_hash(build_dir) {
+            panic!("Proof vkey hash does not match circuit vkey hash, it was generated with a different circuit.");
+        }
+        verify_groth16_bn254(
+            build_dir.to_str().unwrap(),
+            &proof.raw_proof,
+            &vkey_hash.to_string(),
+            &committed_values_digest.to_string(),
+        )
+        .expect("failed to verify proof")
+    }
+}
+
+impl Default for Groth16Bn254Prover {
+    fn default() -> Self {
+        Self::new()
+    }
+}
diff --git a/crates/recursion/gnark-ffi/src/lib.rs b/crates/recursion/gnark-ffi/src/lib.rs
index 670a9b08fe..436739ba83 100644
--- a/crates/recursion/gnark-ffi/src/lib.rs
+++ b/crates/recursion/gnark-ffi/src/lib.rs
@@ -1,9 +1,12 @@
 mod babybear;
 
 pub mod ffi;
-
+pub mod groth16_bn254;
 pub mod plonk_bn254;
+pub mod proof;
 pub mod witness;
 
+pub use groth16_bn254::*;
 pub use plonk_bn254::*;
+pub use proof::*;
 pub use witness::*;
diff --git a/crates/recursion/gnark-ffi/src/plonk_bn254.rs b/crates/recursion/gnark-ffi/src/plonk_bn254.rs
index 0d61a533ea..1ebe0d2e13 100644
--- a/crates/recursion/gnark-ffi/src/plonk_bn254.rs
+++ b/crates/recursion/gnark-ffi/src/plonk_bn254.rs
@@ -7,10 +7,10 @@ use std::{
 use crate::{
     ffi::{build_plonk_bn254, prove_plonk_bn254, test_plonk_bn254, verify_plonk_bn254},
     witness::GnarkWitness,
+    PlonkBn254Proof,
 };
 
 use num_bigint::BigUint;
-use serde::{Deserialize, Serialize};
 use sha2::{Digest, Sha256};
 use sp1_core_machine::SP1_CIRCUIT_VERSION;
 use sp1_recursion_compiler::{
@@ -22,15 +22,6 @@ use sp1_recursion_compiler::{
 #[derive(Debug, Clone)]
 pub struct PlonkBn254Prover;
 
-/// A zero-knowledge proof generated by the PLONK protocol with a Base64 encoded gnark PLONK proof.
-#[derive(Debug, Clone, Serialize, Deserialize, Default)]
-pub struct PlonkBn254Proof {
-    pub public_inputs: [String; 2],
-    pub encoded_proof: String,
-    pub raw_proof: String,
-    pub plonk_vkey_hash: [u8; 32],
-}
-
 impl PlonkBn254Prover {
     /// Creates a new [PlonkBn254Prover].
     pub fn new() -> Self {
@@ -38,7 +29,7 @@ impl PlonkBn254Prover {
     }
 
     pub fn get_vkey_hash(build_dir: &Path) -> [u8; 32] {
-        let vkey_path = build_dir.join("vk.bin");
+        let vkey_path = build_dir.join("plonk_vk.bin");
         let vk_bin_bytes = std::fs::read(vkey_path).unwrap();
         Sha256::digest(vk_bin_bytes).into()
     }
@@ -73,7 +64,7 @@ impl PlonkBn254Prover {
         file.write_all(serialized.as_bytes()).unwrap();
 
         // Write witness.
-        let witness_path = build_dir.join("witness.json");
+        let witness_path = build_dir.join("plonk_witness.json");
         let gnark_witness = GnarkWitness::new(witness);
         let mut file = File::create(witness_path).unwrap();
         let serialized = serde_json::to_string(&gnark_witness).unwrap();
@@ -82,11 +73,12 @@ impl PlonkBn254Prover {
         build_plonk_bn254(build_dir.to_str().unwrap());
 
         // Write the corresponding asset files to the build dir.
-        let sp1_verifier_path = build_dir.join("SP1Verifier.sol");
+        let sp1_verifier_path = build_dir.join("PlonkSP1Verifier.sol");
         let vkey_hash = Self::get_vkey_hash(&build_dir);
         let sp1_verifier_str = include_str!("../assets/SP1Verifier.txt")
             .replace("{SP1_CIRCUIT_VERSION}", SP1_CIRCUIT_VERSION)
-            .replace("{VERIFIER_HASH}", format!("0x{}", hex::encode(vkey_hash)).as_str());
+            .replace("{VERIFIER_HASH}", format!("0x{}", hex::encode(vkey_hash)).as_str())
+            .replace("{PROOF_SYSTEM}", "Plonk");
         let mut sp1_verifier_file = File::create(sp1_verifier_path).unwrap();
         sp1_verifier_file.write_all(sp1_verifier_str.as_bytes()).unwrap();
     }
diff --git a/crates/recursion/gnark-ffi/src/proof.rs b/crates/recursion/gnark-ffi/src/proof.rs
new file mode 100644
index 0000000000..769bc5e0d0
--- /dev/null
+++ b/crates/recursion/gnark-ffi/src/proof.rs
@@ -0,0 +1,25 @@
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub enum ProofBn254 {
+    Plonk(PlonkBn254Proof),
+    Groth16(Groth16Bn254Proof),
+}
+
+/// A zero-knowledge proof generated by the PLONK protocol with a Base64 encoded gnark PLONK proof.
+#[derive(Debug, Clone, Serialize, Deserialize, Default)]
+pub struct PlonkBn254Proof {
+    pub public_inputs: [String; 2],
+    pub encoded_proof: String,
+    pub raw_proof: String,
+    pub plonk_vkey_hash: [u8; 32],
+}
+
+/// A zero-knowledge proof generated by the Groth16 protocol with a Base64 encoded gnark Groth16 proof.
+#[derive(Debug, Clone, Serialize, Deserialize, Default)]
+pub struct Groth16Bn254Proof {
+    pub public_inputs: [String; 2],
+    pub encoded_proof: String,
+    pub raw_proof: String,
+    pub groth16_vkey_hash: [u8; 32],
+}
diff --git a/crates/sdk/src/artifacts.rs b/crates/sdk/src/artifacts.rs
index 97492f1e47..bea4e7a9af 100644
--- a/crates/sdk/src/artifacts.rs
+++ b/crates/sdk/src/artifacts.rs
@@ -6,7 +6,7 @@ use indicatif::{ProgressBar, ProgressStyle};
 use reqwest::Client;
 pub use sp1_prover::build::build_plonk_bn254_artifacts_with_dummy;
 
-use crate::install::try_install_plonk_bn254_artifacts;
+use crate::install::try_install_circuit_artifacts;
 
 /// Exports the solidity verifier for PLONK proofs to the specified output directory.
 ///
@@ -17,16 +17,45 @@ pub fn export_solidity_plonk_bn254_verifier(output_dir: impl Into<PathBuf>) -> R
     let artifacts_dir = if sp1_prover::build::sp1_dev_mode() {
         sp1_prover::build::plonk_bn254_artifacts_dev_dir()
     } else {
-        try_install_plonk_bn254_artifacts()
+        try_install_circuit_artifacts()
     };
-    let verifier_path = artifacts_dir.join("SP1Verifier.sol");
+    let verifier_path = artifacts_dir.join("PlonkSP1Verifier.sol");
 
     if !verifier_path.exists() {
         return Err(anyhow::anyhow!("verifier file not found at {:?}", verifier_path));
     }
 
     std::fs::create_dir_all(&output_dir).context("Failed to create output directory.")?;
-    let output_path = output_dir.join("SP1Verifier.sol");
+    let output_path = output_dir.join("PlonkSP1Verifier.sol");
+    std::fs::copy(&verifier_path, &output_path).context("Failed to copy verifier file.")?;
+    tracing::info!(
+        "exported verifier from {} to {}",
+        verifier_path.display(),
+        output_path.display()
+    );
+
+    Ok(())
+}
+
+/// Exports the solidity verifier for Groth16 proofs to the specified output directory.
+///
+/// WARNING: If you are on development mode, this function assumes that the Groth16 artifacts have
+/// already been built.
+pub fn export_solidity_groth16_bn254_verifier(output_dir: impl Into<PathBuf>) -> Result<()> {
+    let output_dir: PathBuf = output_dir.into();
+    let artifacts_dir = if sp1_prover::build::sp1_dev_mode() {
+        sp1_prover::build::groth16_bn254_artifacts_dev_dir()
+    } else {
+        try_install_circuit_artifacts()
+    };
+    let verifier_path = artifacts_dir.join("Groth16SP1Verifier.sol");
+
+    if !verifier_path.exists() {
+        return Err(anyhow::anyhow!("verifier file not found at {:?}", verifier_path));
+    }
+
+    std::fs::create_dir_all(&output_dir).context("Failed to create output directory.")?;
+    let output_path = output_dir.join("Groth16SP1Verifier.sol");
     std::fs::copy(&verifier_path, &output_path).context("Failed to copy verifier file.")?;
     tracing::info!(
         "exported verifier from {} to {}",
@@ -71,8 +100,16 @@ pub async fn download_file(
 #[cfg(test)]
 mod tests {
     #[test]
-    fn test_verifier_export() {
+    fn test_plonk_verifier_export() {
         crate::artifacts::export_solidity_plonk_bn254_verifier(tempfile::tempdir().unwrap().path())
             .expect("failed to export verifier");
     }
+
+    #[test]
+    fn test_groth16_verifier_export() {
+        crate::artifacts::export_solidity_groth16_bn254_verifier(
+            tempfile::tempdir().unwrap().path(),
+        )
+        .expect("failed to export verifier");
+    }
 }
diff --git a/crates/sdk/src/install.rs b/crates/sdk/src/install.rs
index f3a0c95f0f..36226ecf81 100644
--- a/crates/sdk/src/install.rs
+++ b/crates/sdk/src/install.rs
@@ -7,49 +7,44 @@ use tokio::{runtime, task::block_in_place};
 
 use crate::SP1_CIRCUIT_VERSION;
 
-/// The base URL for the S3 bucket containing the plonk bn254 artifacts.
-pub const PLONK_BN254_ARTIFACTS_URL_BASE: &str = "https://sp1-circuits.s3-us-east-2.amazonaws.com";
-
-/// Gets the directory where the PLONK artifacts are installed.
-fn plonk_bn254_artifacts_dir() -> PathBuf {
-    dirs::home_dir()
-        .unwrap()
-        .join(".sp1")
-        .join("circuits")
-        .join("plonk_bn254")
-        .join(SP1_CIRCUIT_VERSION)
+/// The base URL for the S3 bucket containing the ciruit artifacts.
+pub const CIRCUIT_ARTIFACTS_URL_BASE: &str = "https://sp1-circuits.s3-us-east-2.amazonaws.com";
+
+/// Gets the directory where the circuit artifacts are installed.
+fn circuit_artifacts_dir() -> PathBuf {
+    dirs::home_dir().unwrap().join(".sp1").join("circuits").join(SP1_CIRCUIT_VERSION)
 }
 
-/// Tries to install the PLONK artifacts if they are not already installed.
-pub fn try_install_plonk_bn254_artifacts() -> PathBuf {
-    let build_dir = plonk_bn254_artifacts_dir();
+/// Tries to install the circuit artifacts if they are not already installed.
+pub fn try_install_circuit_artifacts() -> PathBuf {
+    let build_dir = circuit_artifacts_dir();
 
     if build_dir.exists() {
         println!(
-            "[sp1] plonk bn254 artifacts already seem to exist at {}. if you want to re-download them, delete the directory",
+            "[sp1] circuit artifacts already seem to exist at {}. if you want to re-download them, delete the directory",
             build_dir.display()
         );
     } else {
         println!(
-            "[sp1] plonk bn254 artifacts for version {} do not exist at {}. downloading...",
+            "[sp1] circuit artifacts for version {} do not exist at {}. downloading...",
             SP1_CIRCUIT_VERSION,
             build_dir.display()
         );
-        install_plonk_bn254_artifacts(build_dir.clone());
+        install_circuit_artifacts(build_dir.clone());
     }
     build_dir
 }
 
-/// Install the latest plonk bn254 artifacts.
+/// Install the latest circuit artifacts.
 ///
-/// This function will download the latest plonk bn254 artifacts from the S3 bucket and extract them
+/// This function will download the latest circuit artifacts from the S3 bucket and extract them
 /// to the directory specified by [plonk_bn254_artifacts_dir()].
-pub fn install_plonk_bn254_artifacts(build_dir: PathBuf) {
+pub fn install_circuit_artifacts(build_dir: PathBuf) {
     // Create the build directory.
     std::fs::create_dir_all(&build_dir).expect("failed to create build directory");
 
     // Download the artifacts.
-    let download_url = format!("{}/{}.tar.gz", PLONK_BN254_ARTIFACTS_URL_BASE, SP1_CIRCUIT_VERSION);
+    let download_url = format!("{}/{}.tar.gz", CIRCUIT_ARTIFACTS_URL_BASE, SP1_CIRCUIT_VERSION);
     let mut artifacts_tar_gz_file =
         tempfile::NamedTempFile::new().expect("failed to create tempfile");
     let client = Client::builder().build().expect("failed to create reqwest client");
@@ -71,9 +66,8 @@ pub fn install_plonk_bn254_artifacts(build_dir: PathBuf) {
     println!("[sp1] downloaded {} to {:?}", download_url, build_dir.to_str().unwrap(),);
 }
 
-/// The directory where the plonk bn254 artifacts will be stored based on
-/// [PLONK_BN254_ARTIFACTS_VERSION] and [PLONK_BN254_ARTIFACTS_URL_BASE].
-pub fn install_plonk_bn254_artifacts_dir() -> PathBuf {
+/// The directory where the circuit artifacts will be stored.
+pub fn install_circuit_artifacts_dir() -> PathBuf {
     dirs::home_dir().unwrap().join(".sp1").join("circuits").join(SP1_CIRCUIT_VERSION)
 }
 
diff --git a/crates/sdk/src/network/prover.rs b/crates/sdk/src/network/prover.rs
index b512b48cf8..7fc04a09a0 100644
--- a/crates/sdk/src/network/prover.rs
+++ b/crates/sdk/src/network/prover.rs
@@ -182,6 +182,7 @@ impl From<SP1ProofKind> for ProofMode {
             SP1ProofKind::Core => Self::Core,
             SP1ProofKind::Compressed => Self::Compressed,
             SP1ProofKind::Plonk => Self::Plonk,
+            SP1ProofKind::Groth16 => Self::Groth16,
         }
     }
 }
diff --git a/crates/sdk/src/proof.rs b/crates/sdk/src/proof.rs
index ded024913b..390f9197a7 100644
--- a/crates/sdk/src/proof.rs
+++ b/crates/sdk/src/proof.rs
@@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
 use sp1_core_machine::io::{SP1PublicValues, SP1Stdin};
 use strum_macros::{EnumDiscriminants, EnumTryAs};
 
-use sp1_prover::{CoreSC, InnerSC, PlonkBn254Proof};
+use sp1_prover::{CoreSC, Groth16Bn254Proof, InnerSC, PlonkBn254Proof};
 use sp1_stark::{MachineVerificationError, ShardProof};
 
 /// A proof generated with SP1 of a particular proof mode.
@@ -17,6 +17,7 @@ pub enum SP1Proof {
     Core(Vec<ShardProof<CoreSC>>),
     Compressed(ShardProof<InnerSC>),
     Plonk(PlonkBn254Proof),
+    Groth16(Groth16Bn254Proof),
 }
 
 /// A proof generated with SP1, bundled together with stdin, public values, and the SP1 version.
@@ -49,7 +50,7 @@ impl SP1ProofWithPublicValues {
         }
     }
 
-    /// For Plonk proofs, returns the proof in a byte encoding the onchain verifier accepts.
+    /// For Plonk or Groth16 proofs, returns the proof in a byte encoding the onchain verifier accepts.
     /// The bytes consist of the first four bytes of Plonk vkey hash followed by the encoded proof.
     pub fn bytes(&self) -> Vec<u8> {
         match &self.proof {
@@ -61,7 +62,15 @@ impl SP1ProofWithPublicValues {
                 );
                 bytes
             }
-            _ => unimplemented!("only Plonk proofs are verifiable onchain"),
+            SP1Proof::Groth16(groth16_proof) => {
+                let mut bytes = Vec::with_capacity(4 + groth16_proof.encoded_proof.len());
+                bytes.extend_from_slice(&groth16_proof.groth16_vkey_hash[..4]);
+                bytes.extend_from_slice(
+                    &hex::decode(&groth16_proof.encoded_proof).expect("Invalid Groth16 proof"),
+                );
+                bytes
+            }
+            _ => unimplemented!("only Plonk and Groth16 proofs are verifiable onchain"),
         }
     }
 }
diff --git a/crates/sdk/src/proto/network.rs b/crates/sdk/src/proto/network.rs
index 5f36a2bd84..4c3dd84771 100644
--- a/crates/sdk/src/proto/network.rs
+++ b/crates/sdk/src/proto/network.rs
@@ -306,6 +306,8 @@ pub enum ProofMode {
     Compressed = 2,
     /// The proof mode for a PlonK proof.
     Plonk = 3,
+    /// The proof mode for a Groth16 proof.
+    Groth16 = 4,
 }
 impl ProofMode {
     /// String value of the enum field names used in the ProtoBuf definition.
@@ -318,6 +320,7 @@ impl ProofMode {
             ProofMode::Core => "PROOF_MODE_CORE",
             ProofMode::Compressed => "PROOF_MODE_COMPRESSED",
             ProofMode::Plonk => "PROOF_MODE_PLONK",
+            ProofMode::Groth16 => "PROOF_MODE_GROTH16",
         }
     }
     /// Creates an enum from field names used in the ProtoBuf definition.
@@ -327,6 +330,7 @@ impl ProofMode {
             "PROOF_MODE_CORE" => Some(Self::Core),
             "PROOF_MODE_COMPRESSED" => Some(Self::Compressed),
             "PROOF_MODE_PLONK" => Some(Self::Plonk),
+            "PROOF_MODE_GROTH16" => Some(Self::Groth16),
             _ => None,
         }
     }
@@ -706,27 +710,21 @@ impl NetworkServiceClient for twirp::client::Client {
         &self,
         req: GetProofStatusRequest,
     ) -> Result<GetProofStatusResponse, twirp::ClientError> {
-        let url = self
-            .base_url
-            .join("network.NetworkService/GetProofStatus")?;
+        let url = self.base_url.join("network.NetworkService/GetProofStatus")?;
         self.request(url, req).await
     }
     async fn get_proof_requests(
         &self,
         req: GetProofRequestsRequest,
     ) -> Result<GetProofRequestsResponse, twirp::ClientError> {
-        let url = self
-            .base_url
-            .join("network.NetworkService/GetProofRequests")?;
+        let url = self.base_url.join("network.NetworkService/GetProofRequests")?;
         self.request(url, req).await
     }
     async fn get_relay_status(
         &self,
         req: GetRelayStatusRequest,
     ) -> Result<GetRelayStatusResponse, twirp::ClientError> {
-        let url = self
-            .base_url
-            .join("network.NetworkService/GetRelayStatus")?;
+        let url = self.base_url.join("network.NetworkService/GetRelayStatus")?;
         self.request(url, req).await
     }
 }
diff --git a/crates/sdk/src/provers/local.rs b/crates/sdk/src/provers/local.rs
index f891c565b1..c3e16d586a 100644
--- a/crates/sdk/src/provers/local.rs
+++ b/crates/sdk/src/provers/local.rs
@@ -5,7 +5,7 @@ use sp1_prover::{components::SP1ProverComponents, SP1Prover};
 use sysinfo::System;
 
 use crate::{
-    install::try_install_plonk_bn254_artifacts, provers::ProofOpts, Prover, SP1Proof, SP1ProofKind,
+    install::try_install_circuit_artifacts, provers::ProofOpts, Prover, SP1Proof, SP1ProofKind,
     SP1ProofWithPublicValues, SP1ProvingKey, SP1VerifyingKey,
 };
 
@@ -81,22 +81,40 @@ impl<C: SP1ProverComponents> Prover<C> for LocalProver<C> {
         let compress_proof = self.prover.shrink(reduce_proof, opts.sp1_prover_opts)?;
         let outer_proof = self.prover.wrap_bn254(compress_proof, opts.sp1_prover_opts)?;
 
-        let plonk_bn254_aritfacts = if sp1_prover::build::sp1_dev_mode() {
-            sp1_prover::build::try_build_plonk_bn254_artifacts_dev(
-                self.prover.wrap_vk(),
-                &outer_proof.proof,
-            )
-        } else {
-            try_install_plonk_bn254_artifacts()
-        };
-        let proof = self.prover.wrap_plonk_bn254(outer_proof, &plonk_bn254_aritfacts);
         if kind == SP1ProofKind::Plonk {
+            let plonk_bn254_aritfacts = if sp1_prover::build::sp1_dev_mode() {
+                sp1_prover::build::try_build_plonk_bn254_artifacts_dev(
+                    self.prover.wrap_vk(),
+                    &outer_proof.proof,
+                )
+            } else {
+                try_install_circuit_artifacts()
+            };
+            let proof = self.prover.wrap_plonk_bn254(outer_proof, &plonk_bn254_aritfacts);
+
             return Ok(SP1ProofWithPublicValues {
                 proof: SP1Proof::Plonk(proof),
                 stdin,
                 public_values,
                 sp1_version: self.version().to_string(),
             });
+        } else if kind == SP1ProofKind::Groth16 {
+            let groth16_bn254_artifacts = if sp1_prover::build::sp1_dev_mode() {
+                sp1_prover::build::try_build_groth16_bn254_artifacts_dev(
+                    self.prover.wrap_vk(),
+                    &outer_proof.proof,
+                )
+            } else {
+                try_install_circuit_artifacts()
+            };
+
+            let proof = self.prover.wrap_groth16_bn254(outer_proof, &groth16_bn254_artifacts);
+            return Ok(SP1ProofWithPublicValues {
+                proof: SP1Proof::Groth16(proof),
+                stdin,
+                public_values,
+                sp1_version: self.version().to_string(),
+            });
         }
         unreachable!()
     }
diff --git a/crates/sdk/src/provers/mock.rs b/crates/sdk/src/provers/mock.rs
index 9eefd7d9b1..d4d2467045 100644
--- a/crates/sdk/src/provers/mock.rs
+++ b/crates/sdk/src/provers/mock.rs
@@ -13,8 +13,9 @@ use p3_baby_bear::BabyBear;
 use p3_field::{AbstractField, PrimeField};
 use p3_fri::{FriProof, TwoAdicFriPcsProof};
 use sp1_prover::{
-    components::DefaultProverComponents, verify::verify_plonk_bn254_public_inputs, HashableKey,
-    PlonkBn254Proof, SP1Prover,
+    components::DefaultProverComponents,
+    verify::{verify_groth16_bn254_public_inputs, verify_plonk_bn254_public_inputs},
+    Groth16Bn254Proof, HashableKey, PlonkBn254Proof, SP1Prover,
 };
 
 use super::{ProofOpts, ProverType};
@@ -107,6 +108,23 @@ impl Prover<DefaultProverComponents> for MockProver {
                     sp1_version: self.version().to_string(),
                 })
             }
+            SP1ProofKind::Groth16 => {
+                let (public_values, _) = self.prover.execute(&pk.elf, &stdin, context)?;
+                Ok(SP1ProofWithPublicValues {
+                    proof: SP1Proof::Groth16(Groth16Bn254Proof {
+                        public_inputs: [
+                            pk.vk.hash_bn254().as_canonical_biguint().to_string(),
+                            public_values.hash().to_string(),
+                        ],
+                        encoded_proof: "".to_string(),
+                        raw_proof: "".to_string(),
+                        groth16_vkey_hash: [0; 32],
+                    }),
+                    stdin,
+                    public_values,
+                    sp1_version: self.version().to_string(),
+                })
+            }
         }
     }
 
@@ -120,6 +138,10 @@ impl Prover<DefaultProverComponents> for MockProver {
                 verify_plonk_bn254_public_inputs(vkey, &bundle.public_values, public_inputs)
                     .map_err(SP1VerificationError::Plonk)
             }
+            SP1Proof::Groth16(Groth16Bn254Proof { public_inputs, .. }) => {
+                verify_groth16_bn254_public_inputs(vkey, &bundle.public_values, public_inputs)
+                    .map_err(SP1VerificationError::Groth16)
+            }
             _ => Ok(()),
         }
     }
diff --git a/crates/sdk/src/provers/mod.rs b/crates/sdk/src/provers/mod.rs
index e2d999b9b3..faf62126e5 100644
--- a/crates/sdk/src/provers/mod.rs
+++ b/crates/sdk/src/provers/mod.rs
@@ -17,7 +17,7 @@ use strum_macros::EnumString;
 use thiserror::Error;
 
 use crate::{
-    install::try_install_plonk_bn254_artifacts, SP1Proof, SP1ProofKind, SP1ProofWithPublicValues,
+    install::try_install_circuit_artifacts, SP1Proof, SP1ProofKind, SP1ProofWithPublicValues,
 };
 
 /// The type of prover.
@@ -47,6 +47,8 @@ pub enum SP1VerificationError {
     Recursion(MachineVerificationError<InnerSC>),
     #[error("Plonk verification error: {0}")]
     Plonk(anyhow::Error),
+    #[error("Groth16 verification error: {0}")]
+    Groth16(anyhow::Error),
 }
 
 /// An implementation of [crate::ProverClient].
@@ -82,28 +84,41 @@ pub trait Prover<C: SP1ProverComponents>: Send + Sync {
         if bundle.sp1_version != self.version() {
             return Err(SP1VerificationError::VersionMismatch(bundle.sp1_version.clone()));
         }
-        match bundle.proof.clone() {
+        match &bundle.proof {
             SP1Proof::Core(proof) => self
                 .sp1_prover()
-                .verify(&SP1CoreProofData(proof), vkey)
+                .verify(&SP1CoreProofData(proof.clone()), vkey)
                 .map_err(SP1VerificationError::Core),
             SP1Proof::Compressed(proof) => self
                 .sp1_prover()
-                .verify_compressed(&SP1ReduceProof { proof }, vkey)
+                .verify_compressed(&SP1ReduceProof { proof: proof.clone() }, vkey)
                 .map_err(SP1VerificationError::Recursion),
             SP1Proof::Plonk(proof) => self
                 .sp1_prover()
                 .verify_plonk_bn254(
-                    &proof,
+                    proof,
                     vkey,
                     &bundle.public_values,
                     &if sp1_prover::build::sp1_dev_mode() {
                         sp1_prover::build::plonk_bn254_artifacts_dev_dir()
                     } else {
-                        try_install_plonk_bn254_artifacts()
+                        try_install_circuit_artifacts()
                     },
                 )
                 .map_err(SP1VerificationError::Plonk),
+            SP1Proof::Groth16(proof) => self
+                .sp1_prover()
+                .verify_groth16_bn254(
+                    proof,
+                    vkey,
+                    &bundle.public_values,
+                    &if sp1_prover::build::sp1_dev_mode() {
+                        sp1_prover::build::groth16_bn254_artifacts_dev_dir()
+                    } else {
+                        try_install_circuit_artifacts()
+                    },
+                )
+                .map_err(SP1VerificationError::Groth16),
         }
     }
 }