Repository navigation
Conversation
Size the pre-first-squeeze transcript run from every advice phase up to and including the first challenge-bearing phase, not just phase 0, so the valid "advice in an early phase, challenge in a later phase" shape no longer under-sizes the buffer and overruns VK_MPTR. In the quotient VM limb-decomposition and product-add peepholes, re-check that fused opcode coefficients still fit their one-byte constant slots after intervening residue/base emission, falling back to generic ops instead of panicking in the u8::try_from(...).expect(...). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add `QuotientLowering::Direct` as an alternative to the quotient VM (`RenderOptions.quotient_lowering`, default `Vm`, so existing output is unchanged). In Direct mode each gate identity is one Yul function that transliterates its `Expression` tree node by node, in `partially_evaluate_identities` order, and every identity (gates and the permutation, lookup and trash families) is folded explicitly as `bucket += Y_POW[m-1-j] * e`, the weighting of `compute_linearization_commitment`. Scalar constants move from the VM program into a VK constant table. Two sub-tree shapes are recognised from the expression trees, with no circuit parameters hard-coded: linear combinations over a limb vector become `sum_exprs` calls, and limb products whose coefficients depend only on i+j become `pair_wise_prod_by_degree` + `sum_exprs_by_degree`. Checks: - codegen evaluates every identity IR at random points against the original `Expression` and fails closed on a mismatch; - codegen proves each identity is folded exactly once, with the right weight and bucket, including inside the family loops; - a test-only probe render runs the emitted Yul in revm on random evaluation frames against midnight-proofs' own identity values; - trace renders emit intermediate values (helper results, product-table entries, limb views) compared against the original sub-expressions, and return false instead of reverting so mismatches stay visible. Adds a foreign-field/EC fixture circuit. On the Moonlight wrap circuit (B64) Direct verifies in 1,065,440 gas against 1,277,218 for the VM, with a 22,578 B verifier and a 10,081 B VK. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Sep 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
QuotientLowering::Direct, an opt-in alternative to the quotient bytecode VM. The option lives inRenderOptions.quotient_lowering. The default is stillVm, and Vm renders are byte-identical tomain.In Direct mode the quotient numerator is written as Yul that follows the Rust verifier identity by identity:
One function per gate identity. Each gate identity becomes
q_identity_<j>, inpartially_evaluate_identitiesorder. A header gives its index, gate name, polynomial index, bucket andypower. The body transliterates the gate'sExpressiontree node by node, using named constants for evaluation slots, buckets,ypowers and constants.Explicit fold. Every identity is folded as
bucket += Y_POW[m-1-j] * e_j, which is the weightingcompute_linearization_commitmentuses. This covers the gates and the permutation, lookup and trash families (the families keep their existing structured emitters). The gap/tail selector machinery is not used.Two shapes recognised from the expression trees. No circuit parameters are hard-coded; anything that doesn't match stays plain transliteration.
sum_exprs.sum c_ij x_i y_j, where the generator checks thatc_ijdepends only oni+j, becomespair_wise_prod_by_degreefollowed bysum_exprs_by_degree.These are the shapes produced by
sum_exprs/pair_wise_prodincircuits/src/field/foreign/util.rs. Thei+jgrouping is valid because ofdouble_base_powersinparams.rs.Constants live in the VK. Scalar constants and coefficient runs go into the VK payload in place of the VM constants and program.
Motivation: the VM spreads the 49 Moonlight identities over six mechanisms. Most of the gate logic exists only as packed bytecode in the VK contract. That is hard to cross-reference with the Rust code, and it costs about 200k gas per proof. The VM was a bytecode-size measure, not a gas measure.
Checks
Expression::evaluate.0..m-1is folded exactly once, with weightY_POW[m-1-j]and into its manifest bucket. This includes the family loops, which are expanded iteration by iteration. Negative tests cover a duplicated weight, a missing identity, a wrong bucket and family-loop off-by-one errors.[expected_eval, selector_acc[..]]after the quotient section.partially_evaluate_identitiesvalues, folded with the linearization rule. The Vm probe is compared against the same reference.sum_exprsemitter passes codegen and is caught here, with the failing buckets and identities named.30000+j) still match in Direct mode.docs/reference/TRACE_VARIABLES.md: helper results70000+, product-table entries75000+, limb-view words77500+. They are compared against the original sub-expressions evaluated natively.falseinstead of reverting on final-check failure, so a rejected proof's trace survives. Production renders are unchanged.tests/foreign_field_fixture.rsis a secp256k1 foreign-field / EC circuit that exercises the recognisers.Measurements
solc 0.8.35 via-IR, optimizer runs 200, Cancun; anvil with
--hardfork pragueand the default code-size limit.Moonlight wrap verifier (Nightfall
fee_12345, B64; B8 is within a few dozen gas), rendered from the committed production VK and verified with the real fixture proofs:Correctness on Moonlight:
Tests
cargo test --release --lib: 169 passed.HALO2_SOLIDITY_RUN_EVM_TESTS=1 cargo test --release --features evm,rust-verifier-trace --lib: 207 passed.poseidon,rsa,sha_preimage,hybrid_mt,foreign_field) withevm,rust-verifier-traceand withevm,truncated-challenges: pass.ivc_keccak_soliditywithevm,rust-verifier-trace,in-circuit-fewer-point-sets: passes.abi/proof.rs.Limitations / follow-ups
RenderQuotient::Inline;ExternalPinnedis refused.--all-featuressuite and thepbt_tests.This branch is based on
6b3d2096("Fix transcript buffer sizing and quotient VM const-table overflow"), which is not yet onmain, so it is included here.🤖 Generated with Claude Code