Skip to content
Draft
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,6 @@ Cargo.lock
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
circuit_stats_examples/

# Benchmark inputs
noir-examples/noir-passport/merkle_age_check/benchmark-inputs/logs/
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ provekit-verifier-server = { path = "tooling/verifier-server" }
# 3rd party
anyhow = "1.0.93"
argh = "0.1.12"
bincode = "1.3.3"
flate2 = "1.0"
axum = "0.8.4"
base64 = "0.22.1"
bytes = "1.10.1"
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ Analyze PKP file size breakdown:
cargo run --release --bin provekit-cli analyze-pkp ./prover.pkp
```

Check if the proof generated for the public inputs in proof.json/proof.np match the noir circuit.

```sh
nargo execute
cargo run --release --bin provekit-cli check-public-inputs ./target/basic.json ./target/basic.gz ./proof.np
```

Recursively verify in a Gnark proof:

```sh
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/bin/bash

# Requires: Run execute-circuits.sh first to generate witness files

CIRCUITS=(
"t_add_dsc_hash_1300"
"t_add_dsc_verify_1300"
"t_add_id_data_1300"
"t_add_integrity_commit"
"t_attest"
)

cd ../..

LOG_DIR="./benchmark-inputs/logs/check-public-inputs/tbs_1300"
mkdir -p "$LOG_DIR"

strip_ansi() {
sed $'s/\x1b\[[0-9;]*m//g'
}

for circuit in "${CIRCUITS[@]}"; do
echo "Checking public inputs for $circuit"
cargo run --release --bin provekit-cli -- check-public-inputs \
./target/$circuit.json \
./target/$circuit.gz \
./benchmark-inputs/$circuit-proof.np 2>&1 | strip_ansi | tee "$LOG_DIR/$circuit.log"
echo "Checked $circuit"
done
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/bin/bash

CIRCUITS=(
"t_add_dsc_hash_1300"
"t_add_dsc_verify_1300"
"t_add_id_data_1300"
"t_add_integrity_commit"
"t_attest"
)

cd ../..

LOG_DIR="./benchmark-inputs/logs/execute/tbs_1300"
mkdir -p "$LOG_DIR"

strip_ansi() {
sed $'s/\x1b\[[0-9;]*m//g'
}

for circuit in "${CIRCUITS[@]}"; do
echo "Executing $circuit"
cp "./benchmark-inputs/tbs_1300/$circuit.toml" "./$circuit/Prover.toml"
nargo execute --package "$circuit" 2>&1 | strip_ansi | tee "$LOG_DIR/$circuit.log"
rm "./$circuit/Prover.toml"
echo "Executed $circuit"
done
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/bin/bash

# Requires: Run execute-circuits.sh first to generate witness files

CIRCUITS=(
"t_add_dsc_720"
"t_add_id_data_720"
"t_add_integrity_commit"
"t_attest"
)

cd ../..

LOG_DIR="./benchmark-inputs/logs/check-public-inputs/tbs_720"
mkdir -p "$LOG_DIR"

strip_ansi() {
sed $'s/\x1b\[[0-9;]*m//g'
}

for circuit in "${CIRCUITS[@]}"; do
echo "Checking public inputs for $circuit"
cargo run --release --bin provekit-cli -- check-public-inputs \
./target/$circuit.json \
./target/$circuit.gz \
./benchmark-inputs/$circuit-proof.np 2>&1 | strip_ansi | tee "$LOG_DIR/$circuit.log"
echo "Checked $circuit"
done
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/bin/bash

CIRCUITS=(
"t_add_dsc_720"
"t_add_id_data_720"
"t_add_integrity_commit"
"t_attest"
)

cd ../..

LOG_DIR="./benchmark-inputs/logs/execute/tbs_720"
mkdir -p "$LOG_DIR"

strip_ansi() {
sed $'s/\x1b\[[0-9;]*m//g'
}

for circuit in "${CIRCUITS[@]}"; do
echo "Executing $circuit"
cp "./benchmark-inputs/tbs_720/$circuit.toml" "./$circuit/Prover.toml"
nargo execute --package "$circuit" 2>&1 | strip_ansi | tee "$LOG_DIR/$circuit.log"
rm "./$circuit/Prover.toml"
echo "Executed $circuit"
done
19 changes: 9 additions & 10 deletions provekit/common/src/witness/scheduling/splitter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,12 +233,11 @@ impl<'a> WitnessSplitter<'a> {
w1_indices: Vec<usize>,
acir_public_inputs_indices_set: &HashSet<u32>,
) -> Result<Vec<usize>, SplitError> {
let mut public_input_builder_indices = Vec::new();
let mut public_input_builders_with_acir: Vec<(usize, u32)> = Vec::new();
let mut rest_indices = Vec::new();

let w1_indices_set = w1_indices.iter().copied().collect::<HashSet<_>>();

// Build ACIR index -> builder index map for O(1) lookups (O(B) once)
let acir_to_builder: HashMap<u32, usize> = self
.witness_builders
.iter()
Expand All @@ -252,10 +251,7 @@ impl<'a> WitnessSplitter<'a> {
})
.collect();

// Sanity check: all public inputs must have builders in w1 (O(P) lookups)
for &acir_idx in acir_public_inputs_indices_set {
// ACIR witness 0 is always the constant-one witness, handled
// separately via mandatory_w1.insert(0) above — not a regular ACIR witness.
if acir_idx == 0 {
continue;
}
Expand All @@ -271,24 +267,27 @@ impl<'a> WitnessSplitter<'a> {
}
}

// Separate into: 0, public inputs, and rest
for builder_idx in w1_indices {
if builder_idx == 0 {
continue; // Will add 0 first
continue;
} else if let WitnessBuilder::Acir(_, acir_idx) = &self.witness_builders[builder_idx] {
if acir_public_inputs_indices_set.contains(&(*acir_idx as u32)) {
public_input_builder_indices.push(builder_idx);
public_input_builders_with_acir.push((builder_idx, *acir_idx as u32));
continue;
}
}
rest_indices.push(builder_idx);
}

rest_indices.sort_unstable();
public_input_builders_with_acir.sort_unstable_by_key(|&(_, acir_idx)| acir_idx);

// Reorder: 0 first, then public inputs, then rest
let mut new_w1_indices = vec![0];
new_w1_indices.extend(public_input_builder_indices);
new_w1_indices.extend(
public_input_builders_with_acir
.into_iter()
.map(|(idx, _)| idx),
);
new_w1_indices.extend(rest_indices);
Ok(new_w1_indices)
}
Expand Down
10 changes: 5 additions & 5 deletions provekit/prover/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
mod r1cs;
mod whir_r1cs;
mod witness;

use {
crate::{r1cs::R1CSSolver, whir_r1cs::WhirR1CSProver},
acir::native_types::WitnessMap,
anyhow::{Context, Result},
bn254_blackbox_solver::Bn254BlackBoxSolver,
Expand All @@ -10,10 +13,7 @@ use {
std::path::Path,
tracing::instrument,
};

mod r1cs;
mod whir_r1cs;
mod witness;
pub use {r1cs::R1CSSolver, whir_r1cs::WhirR1CSProver};

pub trait Prove {
fn generate_witness(&mut self, input_map: InputMap) -> Result<WitnessMap<NoirElement>>;
Expand Down
164 changes: 164 additions & 0 deletions tooling/cli/src/cmd/check_public_inputs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
use {
super::Command,
acir::{circuit::Program, native_types::WitnessStack},
anyhow::{ensure, Context, Result},
argh::FromArgs,
base64::Engine,
provekit_common::{file::read, utils::noir_to_native, FieldElement, NoirElement, NoirProof},
std::{collections::BTreeSet, fs, path::PathBuf},
tracing::{info, instrument},
};

/// Check that public inputs in a proof match witness values from nargo execute
#[derive(FromArgs, PartialEq, Eq, Debug)]
#[argh(subcommand, name = "check-public-inputs")]
pub struct Args {
/// path to the ACIR circuit file (.json)
#[argh(positional)]
circuit_path: PathBuf,

/// path to the witness file (.gz) from nargo execute
#[argh(positional)]
witness_path: PathBuf,

/// path to the proof file (.np/.json)
#[argh(positional)]
proof_path: PathBuf,
}

impl Command for Args {
#[instrument(skip_all)]
fn run(&self) -> Result<()> {
let (public_indices, return_indices) = load_public_input_indices(&self.circuit_path)?;
let all_public: BTreeSet<u32> = public_indices.union(&return_indices).copied().collect();
info!(
num_public_params = public_indices.len(),
num_return_values = return_indices.len(),
total = all_public.len(),
"Loaded circuit"
);

let witness_values = load_witness_values(&self.witness_path)?;
info!(num_witnesses = witness_values.len(), "Loaded witness file");

let proof: NoirProof = read(&self.proof_path).context("while reading proof")?;
let proof_public_inputs = &proof.public_inputs;
info!(
num_public_inputs = proof_public_inputs.len(),
"Loaded proof"
);

if all_public.is_empty() && proof_public_inputs.is_empty() {
info!("No public inputs - nothing to check");
return Ok(());
}

let mut sorted_indices: Vec<u32> = all_public.iter().copied().collect();
sorted_indices.sort();

let computed_public_inputs: Vec<FieldElement> = sorted_indices
.iter()
.map(|&idx| {
witness_values
.iter()
.find(|(w, _)| w.witness_index() == idx)
.map(|(_, v)| noir_to_native(*v))
.ok_or_else(|| anyhow::anyhow!("Missing witness at index {}", idx))
})
.collect::<Result<Vec<_>>>()?;

ensure!(
computed_public_inputs.len() == proof_public_inputs.len(),
"Public input count mismatch: witness has {} but proof has {}",
computed_public_inputs.len(),
proof_public_inputs.len()
);

println!(
"Verifying {} public inputs:\n",
computed_public_inputs.len()
);

for ((idx, computed), from_proof) in sorted_indices
.iter()
.zip(computed_public_inputs.iter())
.zip(proof_public_inputs.0.iter())
{
let input_type = if return_indices.contains(idx) {
"return"
} else {
"param"
};

ensure!(
computed == from_proof,
"Public input mismatch at w{} ({}):\n witness: {:?}\n proof: {:?}",
idx,
input_type,
computed,
from_proof
);

println!(" w{} ({}): ✓ match", idx, input_type);
}

println!();
info!(
count = computed_public_inputs.len(),
"All public inputs match!"
);
Ok(())
}
}

fn load_public_input_indices(path: &PathBuf) -> Result<(BTreeSet<u32>, BTreeSet<u32>)> {
let json_string = fs::read_to_string(path)
.with_context(|| format!("while reading circuit file `{}`", path.display()))?;

let json: serde_json::Value =
serde_json::from_str(&json_string).context("while parsing circuit JSON")?;

let bytecode_str = json["bytecode"]
.as_str()
.context("expected 'bytecode' field in circuit JSON")?;

let bytecode = base64::prelude::BASE64_STANDARD
.decode(bytecode_str)
.context("while decoding base64 bytecode")?;

let program: Program<NoirElement> =
Program::deserialize_program(&bytecode).context("while deserializing ACIR program")?;

let circuit = program
.functions
.first()
.context("program has no functions")?;

let public_params: BTreeSet<u32> = circuit
.public_parameters
.0
.iter()
.map(|w| w.witness_index())
.collect();

let return_values: BTreeSet<u32> = circuit
.return_values
.0
.iter()
.map(|w| w.witness_index())
.collect();

Ok((public_params, return_values))
}

fn load_witness_values(path: &PathBuf) -> Result<Vec<(acir::native_types::Witness, NoirElement)>> {
let compressed_bytes = fs::read(path)
.with_context(|| format!("while reading witness file `{}`", path.display()))?;

let mut witness_stack: WitnessStack<NoirElement> = WitnessStack::deserialize(&compressed_bytes)
.context("while deserializing witness stack")?;

let stack_item = witness_stack.pop().context("witness stack is empty")?;

Ok(stack_item.witness.into_iter().collect())
}
Loading
Loading