Skip to content

feat: add Poseidon support, pipeline for Intermediate Representation and gnark verifier transpilation target#1322

Merged
markosg04 merged 113 commits intoa16z:mainfrom
defi-wonderland:feat/verifier-transpilation-gnark
Apr 7, 2026
Merged

feat: add Poseidon support, pipeline for Intermediate Representation and gnark verifier transpilation target#1322
markosg04 merged 113 commits intoa16z:mainfrom
defi-wonderland:feat/verifier-transpilation-gnark

Conversation

@0xParti
Copy link
Copy Markdown
Contributor

@0xParti 0xParti commented Mar 5, 2026

Jolt Verifier Transpilation Pipeline

Summary

Symbolic transpilation pipeline that converts Jolt's Rust verifier (stages 1-7) into a target-agnostic intermediate representation. First code generation target is gnark/Groth16.

Related: Builds on top of zklean-extractor/ from PR #684

Motivation

Recursive proof verification requires running the Jolt verifier inside a circuit. Manually rewriting the verifier in Go would be:

  • Error-prone: The verifier has ~20 sumcheck instances across 7 stages
  • Unmaintainable: Every upstream change would require manual porting
  • Expensive: Months of engineering effort with no guarantee of correctness

Instead, we run the original Rust verifier with a symbolic type (MleAst) that records operations instead of computing them. The recorded AST can then be mechanically translated to any circuit framework.

Architecture

The pipeline builds on zklean's MleAst symbolic type (PR #684), which implements JoltField but records operations as an AST instead of computing them. We extended this foundation with:

  • Thread-local storage for transcript integration (challenges, commitments, points)
  • Constraint mode: PartialEq impl records assertions instead of comparing
  • New node variants: Poseidon hashing, byte truncation
  • Per-constraint expression trees with isolated CSE (see below)
┌─────────────────────────────────────────────────────────────────┐
│                         RUST SIDE                               │
├─────────────────────────────────────────────────────────────────┤
│  JoltProof<Fr> (concrete field elements)                        │
│       │                                                         │
│       ▼                                                         │
│  JoltProof<MleAst> (symbolic variables)                         │
│       │                                                         │
│       ▼                                                         │
│  Run verifier stages 1-7 with symbolic transcript               │
│       │                                                         │
│       ▼                                                         │
│  NODE_ARENA (recorded AST + equality assertions)                │
│       │                                                         │
│       ├──► JSON IR (for analysis or other backends)             │
│       │                                                         │
│       ▼                                                         │
│  Code generation → Go circuit + witness                         │
│       │                                                         │
└───────┼─────────────────────────────────────────────────────────┘
        │
        ▼
┌─────────────────────────────────────────────────────────────────┐
│                          GO SIDE                                │
├─────────────────────────────────────────────────────────────────┤
│  gnark circuit + witness values                                 │
│       │                                                         │
│       ▼                                                         │
│  Groth16 setup, prove, verify                                   │
└─────────────────────────────────────────────────────────────────┘

Key components we built:

Component Purpose Why
TranspilableVerifier Dedicated verifier for stages 1-7 The main JoltVerifier handles ZK variants, stage 8, and recursion. A separate module avoids polluting the main verifier with transpilation conditionals.
OpeningAccumulator trait Generic accumulator interface Stage verifiers call accumulator.append_*() to collect opening claims. Making this a trait lets us swap the real accumulator with a symbolic one. Required adding A: OpeningAccumulator<F> to ~25 stage verifiers.
PoseidonAstTranscript Symbolic Fiat-Shamir Implements the Transcript trait but records Poseidon operations as AST nodes. Uses thread-local storage to tunnel MleAst values through from_bytes() calls.
Poseidon transcript SNARK-friendly hash Added to jolt-core so proofs can use Poseidon instead of Blake2b/Keccak, enabling efficient in-circuit verification.

Per-constraint expression trees: zklean uses a single global AST, which works well for their Lean4 extraction use case. For Jolt transpilation with transcript-derived challenges, we found it easier to build N independent expression trees (one per equality assertion). Each tree has its own CSE context (cse_0_*, cse_1_*, etc.), making debugging simpler: when a constraint fails, all its CSE variables are self-contained. Converting to a single tree is straightforward if needed.

The IR is target-agnostic: adding a new backend (e.g., Circom, Plonky2) only requires implementing AST traversal and code emission for that target.

Poseidon Implementation

Both Rust and Go use circom-compatible BN254 Poseidon parameters (x^5 S-box, 8 full rounds, 56 partial rounds). Constants and test vectors are extracted from the same source to guarantee parity.

Side Source Details
Rust light-poseidon v0.4.0 Used directly via Poseidon::<Fr>::new_circom(width). Also used to extract round constants and MDS matrices for Go.
Go Adapted from succinctlabs/gnark-plonky2-verifier/poseidon Removed the Goldilocks field dependency (not needed for BN254). Constants injected from light-poseidon via extract_poseidon_constants binary.

Test vectors are generated from Rust (cargo run -p transpiler --bin poseidon_vectors) and verified in Go tests, ensuring both implementations produce identical outputs.

Scope

Included:

  • Stages 1-7 (all sumcheck verification), including AdviceClaimReduction
  • Poseidon transcript (SNARK-friendly Fiat-Shamir)
  • gnark/Groth16 code generation

Excluded (intentionally):

  • Stage 8 (PCS): Dory uses pairings that are too expensive in-circuit. Will be implemented natively in the target framework.
  • ZK proofs: The transpilable verifier panics on ZK variants. Will be added in a future commit.

Changes

New crate: transpiler/

File Description
src/main.rs Entry point: proof loading, symbolic execution orchestration
src/gnark_codegen.rs AST to Go code generation with per-constraint CSE
src/symbolic_proof.rs Proof symbolization (concrete values to MleAst variables)
src/symbolize.rs IO symbolization for universal circuits
src/symbolic_traits/ PoseidonAstTranscript, AstOpeningAccumulator, AstCommitmentScheme

Modified: zklean-extractor/

File Description
src/mle_ast.rs Thread-local storage, constraint mode, new Node variants
src/ast_bundle.rs Serializable IR with per-constraint CSE (new)
src/scalar_ops.rs BN254 modular arithmetic for constant folding (new)

Modified: jolt-core/

File Description
src/transcripts/poseidon.rs Poseidon transcript for SNARK-friendly Fiat-Shamir (new)
src/zkvm/transpilable_verifier.rs Verifier stages 1-7, omitting ZK and stage 8 (new)
src/poly/opening_proof.rs Made OpeningAccumulator a trait
Stage verifiers (~25 files) Added generic accumulator parameter

New: transpiler/go/

File Description
poseidon/ Go Poseidon implementation matching jolt-core
e2e_test.go Full pipeline test (build, prove, transpile, verify)
stages_circuit_test.go Solver and Groth16 tests
stages_circuit.go Generated gnark circuit
stages_witness.json Generated witness values

Review Guide

Critical paths:

  • symbolic_traits/poseidon.rs: Fiat-Shamir transcript must produce identical challenges in Rust and Go
  • mle_ast.rs constraint mode: The PartialEq impl records equality assertions instead of comparing values
  • gnark_codegen.rs: CSE correctness and iterative AST traversal (avoids stack overflow on deep trees)

Architectural decisions:

  • OpeningAccumulator trait: Enables swapping concrete accumulator with symbolic one across ~25 stage verifiers
  • Thread-local tunneling in mle_ast.rs: Passes MleAst values through type boundaries where generics aren't possible (e.g., from_bytes())

Testing

# Full E2E pipeline (builds binaries, generates proof, transpiles, runs Groth16)
cd transpiler/go && go test -v -run TestEndToEndPipeline -timeout 30m

# IR output only (for analysis or other backends)
cargo run -p transpiler --release --features transcript-poseidon -- -t ast-bundle -o ./output

# Just the Groth16 prove/verify (assumes circuit already generated)
cd transpiler/go && go test -v -run TestStagesCircuitProveVerify -timeout 10m

Known Limitations

  1. No advice support in transpiler binary: The TranspilableVerifier supports advice, but the main.rs doesn't pass advice commitments (the fibonacci example doesn't use them).

  2. No ZK mode: The TranspilableVerifier panics on ZK proof variants. ZK mode was added recently and will be incorporated in a future commit.

  3. Circuit universality: The generated Groth16 circuit size depends on trace length and bytecode size. Each program configuration requires its own trusted setup. To avoid per-program setups, we plan to define universality classes: groups of programs with similar parameters that share the same circuit and setup, with acceptable padding overhead for smaller programs. IR generation will not get affected by future changes on this direction.

Future Work

  • ZK proof support in TranspilableVerifier
  • Stage 8 (PCS) native implementation (pending final PCS choice)
  • Universal circuit with configurable IO sizes

@0xParti 0xParti marked this pull request as draft March 5, 2026 13:06
@akoidefi akoidefi force-pushed the feat/verifier-transpilation-gnark branch from 3a28852 to 920db4a Compare March 9, 2026 10:20
@0xParti 0xParti force-pushed the feat/verifier-transpilation-gnark branch from eb13040 to c31c8c4 Compare March 9, 2026 14:25
@0xParti 0xParti marked this pull request as ready for review March 14, 2026 22:47
Copy link
Copy Markdown
Collaborator

@moodlezoup moodlezoup left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amazing work, just a few questions and nits

Comment on lines +246 to +257
fn challenge_scalar<JF: JoltField>(&mut self) -> JF {
self.challenge_scalar_128_bits()
}

fn challenge_scalar_128_bits<JF: JoltField>(&mut self) -> JF {
// Full 32-byte hash output = full Fr challenge (no truncation).
// challenge_bytes(32) → challenge_bytes32 → one hash invocation.
// from_le_bytes_mod_order(serialize_le(Fr)) = Fr (identity).
let mut buf = vec![0u8; 32];
self.challenge_bytes(&mut buf);
JF::from_bytes(&buf)
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fn challenge_scalar<JF: JoltField>(&mut self) -> JF {
self.challenge_scalar_128_bits()
}
fn challenge_scalar_128_bits<JF: JoltField>(&mut self) -> JF {
// Full 32-byte hash output = full Fr challenge (no truncation).
// challenge_bytes(32) → challenge_bytes32 → one hash invocation.
// from_le_bytes_mod_order(serialize_le(Fr)) = Fr (identity).
let mut buf = vec![0u8; 32];
self.challenge_bytes(&mut buf);
JF::from_bytes(&buf)
}
fn challenge_scalar<JF: JoltField>(&mut self) -> JF {
// Full 32-byte hash output = full Fr challenge (no truncation).
// challenge_bytes(32) → challenge_bytes32 → one hash invocation.
// from_le_bytes_mod_order(serialize_le(Fr)) = Fr (identity).
let mut buf = vec![0u8; 32];
self.challenge_bytes(&mut buf);
JF::from_bytes(&buf)
}
fn challenge_scalar_128_bits<JF: JoltField>(&mut self) -> JF {
unimplemented!("128-bit challenges are unsupported for PoseidonTranscript");
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread jolt-core/src/transcripts/poseidon.rs Outdated
Comment on lines +276 to +278
// Full Fr challenge via challenge_scalar_128_bits, then wrap in Challenge type.
// Mont254BitChallenge<F> is a newtype of F → same memory layout → transmute is safe.
let scalar: JF = self.challenge_scalar_128_bits();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Full Fr challenge via challenge_scalar_128_bits, then wrap in Challenge type.
// Mont254BitChallenge<F> is a newtype of F → same memory layout → transmute is safe.
let scalar: JF = self.challenge_scalar_128_bits();
// Full Fr challenge via challenge_scalar, then wrap in Challenge type.
// Mont254BitChallenge<F> is a newtype of F → same memory layout → transmute is safe.
let scalar: JF = self.challenge_scalar();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread jolt-core/src/poly/opening_proof.rs Outdated
sumcheck: SumcheckId,
) -> Option<(OpeningPoint<BIG_ENDIAN, F>, F)>;

// === Methods for generic verifier (transpilation support) ===
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe add these methods to a new AbstractVerifierOpeningAccumulator<F: JoltField>: OpeningAccumulator<F> trait

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we addressed this in eff5f8c
We had to changed multiple files that imported this

Comment on lines +164 to +166
// =============================================================================
// Serialization Implementations (required by trait bounds)
// =============================================================================
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I think for empty structs, we should be able to #[derive(CanonicalSerialize, CanonicalDeserialize)]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, fixed in 8ee7634

Comment thread transpiler/src/lib.rs Outdated
//! Currently supported:
//! - **gnark**: Go/Groth16 circuit generation (~250 constraints per Poseidon hash)
//!
//! esto es un delirio absoluto de claudio
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤣

Copy link
Copy Markdown
Contributor Author

@0xParti 0xParti Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😅 lol b758587

Comment thread zklean-extractor/src/mle_ast.rs Outdated
Comment on lines +1410 to +1411
// Fallback: create constant from bytes (for non-transpilation use)
MleAst::from_i128(0)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this right? Should we just panic here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed 48db42f

Comment thread transpiler/go/poseidon/poseidon.go Outdated
Comment on lines +15 to +16
// pow2 contains precomputed powers of 2 for bit recomposition.
var pow2 [256]*big.Int
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think this is unused

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch! c63989b

Comment on lines +194 to +195
// Fallback for non-MleAst types (shouldn't happen in transpilation)
self.hash_and_update(MleAst::from_u64(0));
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we panic here instead?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right, addressed 48db42f

Comment thread transpiler/src/symbolize.rs Outdated
Comment on lines +67 to +68
// Panic is NOT pushed here — fiat_shamir_preamble sends it through
// append_u64, which calls raw_append_u64 (bypasses raw_append_bytes/FIFO).
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused by this –– doesn't this mean that panic is not fully symbolic in the circuit?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Panic is fully symbolic. It enters the circuit through two paths:

  1. Transcript: raw_append_u64 hashes it as a concrete constant into the Poseidon state (correct for Fiat-Shamir since the value is already committed).
  2. RAM MLE: allocated as witness variable io_panic_val, used by eval_io_mle for the panic contribution and termination checks.

Our comment was confusing because "NOT pushed here" referred to the byte-chunk FIFO, not to the circuit. Updated the comment to clarify da017a7

"emulated.Element[emulated.BN254Fp]"
}
};
output.push_str(&format!("\t{field_name} {go_type} `gnark:\",public\"`\n"));
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I may be missing something, but aren't we making all of the circuit inputs public by doing this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed on the call. All inputs are intentionally public right now because without PCS verification in the circuit (deferred pending PCS choice), commitments and proof data need to be externally verifiable. Once the PCS is integrated, we'll split public/private using the existing WitnessType infrastructure in AstBundle. The exact public surface still needs investigation since some inputs may benefit from staying public to save in-circuit range checks. Added a comment in codegen documenting this 9af2a73

@moodlezoup
Copy link
Copy Markdown
Collaborator

This looks good to me; will merge once conflicts are resolved

@akoidefi akoidefi force-pushed the feat/verifier-transpilation-gnark branch from 9af2a73 to 31017f3 Compare March 31, 2026 01:40
@socket-security
Copy link
Copy Markdown

socket-security bot commented Mar 31, 2026

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedgolang/​github.com/​consensys/​gnark@​v0.10.07877100100100
Addedgolang/​github.com/​consensys/​gnark-crypto@​v0.13.07685100100100
Addedcargo/​light-poseidon@​0.4.010010093100100

View full report

@socket-security
Copy link
Copy Markdown

socket-security bot commented Mar 31, 2026

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
High CVE: gnark-crypto allows unchecked memory allocation during vector deserialization in golang github.com/consensys/gnark-crypto

CVE: GHSA-fj2x-735w-74vq gnark-crypto allows unchecked memory allocation during vector deserialization (HIGH)

Affected versions: = 0.19.0; >= 0.9.1 < 0.18.1; >= 0.19.0 < 0.19.2

Patched version: 0.18.1

From: transpiler/go/go.modgolang/github.com/consensys/gnark-crypto@v0.13.0

ℹ Read more on: This package | This alert | What is a CVE?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Remove or replace dependencies that include known high severity CVEs. Consumers can use dependency overrides or npm audit fix --force to remove vulnerable dependencies.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore golang/github.com/consensys/gnark-crypto@v0.13.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
High CVE: gnark commitments to private witnesses in Groth16 as implemented break zero-knowledge property in golang github.com/consensys/gnark

CVE: GHSA-9xcg-3q8v-7fq6 gnark commitments to private witnesses in Groth16 as implemented break zero-knowledge property (HIGH)

Affected versions: < 0.11.0; < 0.11.0

Patched version: 0.11.0

From: transpiler/go/go.modgolang/github.com/consensys/gnark@v0.10.0

ℹ Read more on: This package | This alert | What is a CVE?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Remove or replace dependencies that include known high severity CVEs. Consumers can use dependency overrides or npm audit fix --force to remove vulnerable dependencies.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore golang/github.com/consensys/gnark@v0.10.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
High CVE: gnark is vulnerable to signature malleability in EdDSA and ECDSA due to missing scalar checks in golang github.com/consensys/gnark

CVE: GHSA-95v9-hv42-pwrj gnark is vulnerable to signature malleability in EdDSA and ECDSA due to missing scalar checks (HIGH)

Affected versions: < 0.14.0

Patched version: 0.14.0

From: transpiler/go/go.modgolang/github.com/consensys/gnark@v0.10.0

ℹ Read more on: This package | This alert | What is a CVE?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Remove or replace dependencies that include known high severity CVEs. Consumers can use dependency overrides or npm audit fix --force to remove vulnerable dependencies.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore golang/github.com/consensys/gnark@v0.10.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

akoidefi and others added 17 commits March 31, 2026 21:54
Adds PoseidonTranscript using light_poseidon over BN254.

Uses width-3 Poseidon to include n_rounds in every hash call for domain
separation, same as Blake2b/Keccak. Chunks large inputs into 32-byte
pieces since Poseidon has fixed-width inputs.

Gated behind transcript-poseidon feature flag.
Co-authored-by: Markos <53157953+markosg04@users.noreply.github.com>
- Add PoseidonParams<F> trait for type-level parameter configuration
- Implement FrParams (uses new_circom) and FqParams (generated params)
- Add poseidon_fq_params.rs with BN254 Fq parameters (128-bit security)
- Create type aliases PoseidonTranscriptFr and PoseidonTranscriptFq
- Fq params generated with poseidon-paramgen v0.4.0 (audited by NCC Group)

This enables SNARK composition where sumchecks operate over Fq.
Stage 1 now works end-to-end with Groth16 prove/verify.

The main issue was using the wrong transcript - proof was generated with
Blake2b (default) but Go circuit uses Poseidon. Fixed by documenting that
--features transcript-poseidon is required for gnark transpilation.

Changes:
- Add debug-expected-output feature flag for transcript debugging
- Add detailed debug output for challenge derivation (behind feature flag)
- Clean up old stage1_* files (replaced by stages16)
- Remove obsolete debug tools and witness loader
- Improve codegen and mle_ast for per-assertion CSE
…register claim reduction)

Stage 3 adds 3 batched sumchecks to the transpiled verifier:
- Spartan shift: verifies next-cycle values match trace
- Instruction input: verifies operand constraints
- Register claim reduction: batches register access claims

Results (Stages 1-3):
- Assertions: 5 (up from 4)
- Constraints: 763,394 (+231,208 from Stage 2)
- Groth16 prove: ~1.85s
- Groth16 verify: ~1.44ms
Stage 4 adds 4 batched sumchecks:
- Register read/write checking
- RAM ra booleanity
- RAM val evaluation
- RAM val final

Results (Stages 1-4):
- Assertions: 10 (up from 5)
- Constraints: 1,048,560 (+285,166 from Stage 3)
- Groth16 prove: ~2.5s
- Groth16 verify: ~1.5ms

Bug fix: Added memoization to count_mul_by_zero() in transpile_stages.rs.
Without memoization, the function hung for 6+ minutes due to exponential
traversal of shared AST nodes (common subexpressions).
Stage 5 (Register val evaluation, RAM Hamming booleanity, RAM ra reduction,
Lookups read-raf) now passes with 13 assertions and 1,531,516 constraints.

Fixed stack overflow during code generation by converting recursive
 and  functions to iterative implementations
using explicit stacks. Stage 5's deeper AST exceeded stack limits even
with 128MB stack size.

Results:
- Assertions: 13 (up from 10 in Stage 4)
- Constraints: 1,531,516 (+482,956 from Stage 4)
- Proof size: 164 bytes
- Prove time: 3.78s
- Verify time: 1.59ms
Add comprehensive tests to verify the generated circuit is performing
real cryptographic verification and not passing blindly:

- TestCorruptedWitnessRejected: Targeted corruption of commitment,
  PC claims, register values, RAM booleanity - all correctly rejected
- TestRandomFuzzing: 20 random field corruptions, 100% rejection rate
- TestAssertionCountMatchesTheory: Verifies 13 assertions match expected
- TestCircuitNotTrivial: Validates 1.5M constraints, 5924 Poseidon hashes,
  11839 multiplications - confirms real cryptographic work

These tests provide confidence that the transpiled circuit is sound.
…hashing

Add tools for benchmarking different Poseidon widths:
- extract_poseidon_constants.rs: Generate Go constants for any width
- poseidon_vectors.rs: Generate test vectors for verification
- constants4.go, constants5.go: Pre-generated width-4/5 constants

Benchmark results: width-4 (3 inputs) is optimal (+12.8% constraints for width-5).

Also applies semantic fix to PoseidonAstTranscript: append_message,
append_u64, append_scalar now use direct poseidon calls matching
jolt-core's immediate hashing semantics.
Remove functions and binaries that depended on the old stage1_only_verifier:
- Remove generate_stage1_circuit* functions from codegen.rs
- Remove export_stage1_ast/export_stage1_poseidon_ast from ast_json.rs
- Delete verify_final_claim.rs binary
- Delete run_full_stage1.rs binary (was broken)
- Update lib.rs exports

The gnark-transpiler now uses transpilable_verifier exclusively for
stages 1-5 verification.
Remove unused functions and imports that were left behind after the
stage1_only_verifier cleanup:

codegen.rs:
- Remove unused imports (Bindings, common_subexpression_elimination_incremental, insert_node)
- Remove unused method edge_to_gnark on MemoizedCodeGen
- Remove dead CSE helper functions (~260 lines):
  - atom_to_gnark_with_offset
  - edge_to_gnark_with_offset
  - generate_gnark_expr_with_vars_and_offset
  - generate_gnark_expr_for_node_with_offset
  - generate_gnark_expr_with_cse
- Fix unused variable warning with matches!() macro

ast_json.rs:
- Remove unused import get_node
- Remove dead tree traversal functions (~80 lines):
  - collect_nodes_from_root
  - collect_vars_from_node
  - collect_vars_from_edge

transpile_stages.rs:
- Remove unused imports (ark_ff::PrimeField, serde::Serialize)
- Fix unused mut warning on accumulator
- Fix unused variables (indent, Poseidon match arms)
0xParti and others added 24 commits March 31, 2026 21:55
Co-Authored-By: akoi <197815935+akoidefi@users.noreply.github.com>
Enable transpilation of Jolt programs that use TrustedAdvice (e.g.,
merkle-tree example). The key fix is in GenericRamValCheckVerifier::new
which now computes init_eval using eval_initial_ram_mle + advice
contributions instead of a direct polynomial evaluation that missed
advice regions.

Changes:
- merkle-tree example: add --save flag and transcript feature flags
- transpiler main.rs: add --trusted-advice CLI arg to load and
  symbolize advice commitments
- transpilable_verifier.rs: fix init_eval to include advice selector *
  advice_eval terms via compute_advice_init_contributions
- gnark_codegen.rs: split large constraints into sub-functions and
  cap inline expression size to prevent Go compiler OOM
…umulator

After rebasing onto upstream/main, VerifierOpeningAccumulator gained
deduplication logic (populate_or_alias_opening) that aliases openings
when the same polynomial is opened at the same evaluation point by
different sumcheck stages. AstOpeningAccumulator didn't handle this,
pushing extra zero claims to pending_claims and corrupting the
Fiat-Shamir transcript.

Replicate the aliasing logic 1:1: Case A (key in proof) aliases if
same poly already opened at same point, otherwise stores normally.
Case B (key not in proof) aliases to matching opening or creates with
zero claim. Point comparison uses AST NodeId equality.
- Remove unused deps ark-ec and serde from transpiler (machete)
- Add zk feature to transpiler so cfg guards propagate correctly
- Gate symbolize_proof body and helpers with #[cfg(not(feature = "zk"))]
  to handle JoltProof conditional fields (opening_claims vs blindfold_proof)
- Add zk_generator fields to JoltVerifierPreprocessing in main.rs
- Inline format string variables in transpiler/main.rs (clippy)
- Refactor zklean-extractor main.rs to use library imports instead of
  mod re-declarations (fixes dead_code warnings and module resolution)
- Export lookup_table_flags and sumchecks from zklean-extractor lib.rs
- Add #[allow(clippy::op_ref)] on test that intentionally tests &refs
- Add scripts/ci-local.sh for local CI verification
Make IO values (inputs, outputs, panic) symbolic witness variables instead
of hardcoded constants in the generated Groth16 circuit. This is the first
step toward universality classes (same circuit for same-shaped programs).

jolt-core changes (non-breaking, additive only):
- Add SparseEvalCoeff trait with u64 (Barrett) and F: JoltField (direct mul) impls
- Rename sparse_eval_u64_block → sparse_eval_block<F, V: SparseEvalCoeff<F>>
- Add thread-local overrides (PENDING_IO_MLE, PENDING_INITIAL_RAM)
- Add early returns in eval_io_mle/eval_initial_ram_mle for symbolic path

transpiler changes:
- New io_replay.rs: FIFO queue for transcript byte overrides
- poseidon.rs: raw_append_bytes consumes FIFO, raw_append_label_with_len bypasses it
- New symbolize.rs: symbolize_io_device() creates symbolic IO variables
- main.rs: integrate symbolic IO + PENDING_INITIAL_RAM before verify()

E2E verified: 2,777,232 constraints, 164-byte Groth16 proof, 2ms verify.
- Remove unused OpeningAccumulator import in univariate_skip.rs
- Fix long line formatting in verifier.rs, main.rs, ast_commitment_scheme.rs
cargo fmt for AbstractVerifierOpeningAccumulator long lines, remove
duplicate Blake2bTranscript import, update fiat_shamir_preamble call
in transpilable_verifier to match upstream 8-arg signature.
@akoidefi akoidefi force-pushed the feat/verifier-transpilation-gnark branch from 74c625e to 5f274c4 Compare March 31, 2026 20:05
@0xParti
Copy link
Copy Markdown
Contributor Author

0xParti commented Mar 31, 2026

This looks good to me; will merge once conflicts are resolved

@moodlezoup this is ready to merge now

akoidefi added 2 commits April 7, 2026 10:45
The imports for JoltCurve, SumcheckInstanceProof, UniSkipFirstRoundProofVariant,
and CompressedUniPoly were gated with #[cfg(not(feature = "zk"))], but the two
functions using them (symbolize_uni_skip_variant, symbolize_sumcheck_variant)
were not. CI runs clippy with --features zk which removes the imports but leaves
the functions, causing "cannot find type" errors.
…pilation-gnark

# Conflicts:
#	Cargo.lock
#	jolt-core/src/zkvm/ram/read_write_checking.rs
@markosg04 markosg04 merged commit c0cf7e4 into a16z:main Apr 7, 2026
21 of 22 checks passed
@0xParti 0xParti mentioned this pull request Apr 7, 2026
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants