fix(circuit): fill the failure guarantee with the real nullifier and modulus - #1658
Open
TomerStarkware wants to merge 1 commit into
Open
fix(circuit): fill the failure guarantee with the real nullifier and modulus#1658TomerStarkware wants to merge 1 commit into
TomerStarkware wants to merge 1 commit into
Conversation
…modulus `eval_circuit`'s failure branch and `circuit_failure_guarantee_verify` returned `llvm::undef` for the failure guarantee, so the corelib walk that destructs it (`u96_limbs_less_than_guarantee_verify`) branched on poison instead of on the guarantee's limbs. The walk's stop position determines how much gas is redeposited on the failure path of contracts (compiled with optimizations, each arm carries a `redeposit_gas`), so native's remaining gas diverged from the VM after any failing `circuit_inverse` (e.g. inverting 0): 380 gas for the BLS12-381 modulus, 90 for secp256k1, and around the exhaustion boundary the VM succeeds where native runs out of gas. `CircuitFailureGuarantee` is now a real value carrying the nullifier and modulus limbs: the failing inverse gate's gcd is forwarded to the error block, which materializes `nullifier = modulus / gcd(input, modulus)` - exactly the value the VM's hint computes - and `circuit_failure_guarantee_verify` unpacks it into the `U96LimbsLtGuarantee<4>` output. The regression test destructs the guarantee manually (the corelib-private walk API is importable in single-file programs) and returns the walk length as a value, comparing it against the VM; two moduli with different expected lengths catch any walk not steered by the actual limb values. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Benchmarking resultsBenchmark for program
|
| Command | Mean [s] | Min [s] | Max [s] | Relative |
|---|---|---|---|---|
Cairo-vm (Rust, Cairo 1) |
10.505 ± 0.042 | 10.422 | 10.552 | 5.68 ± 0.04 |
cairo-native (embedded AOT) |
1.850 ± 0.011 | 1.837 | 1.871 | 1.00 |
cairo-native (embedded JIT using LLVM's ORC Engine) |
1.875 ± 0.012 | 1.857 | 1.891 | 1.01 ± 0.01 |
Benchmark for program dict_snapshot
Open benchmarks
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
Cairo-vm (Rust, Cairo 1) |
540.8 ± 8.2 | 530.6 | 557.3 | 1.00 |
cairo-native (embedded AOT) |
1687.7 ± 10.6 | 1671.0 | 1708.2 | 3.12 ± 0.05 |
cairo-native (embedded JIT using LLVM's ORC Engine) |
1712.0 ± 14.2 | 1695.1 | 1740.4 | 3.17 ± 0.05 |
Benchmark for program factorial_2M
Open benchmarks
| Command | Mean [s] | Min [s] | Max [s] | Relative |
|---|---|---|---|---|
Cairo-vm (Rust, Cairo 1) |
4.692 ± 0.015 | 4.671 | 4.708 | 2.69 ± 0.04 |
cairo-native (embedded AOT) |
1.746 ± 0.026 | 1.715 | 1.795 | 1.00 |
cairo-native (embedded JIT using LLVM's ORC Engine) |
1.767 ± 0.020 | 1.741 | 1.804 | 1.01 ± 0.02 |
Benchmark for program fib_2M
Open benchmarks
| Command | Mean [s] | Min [s] | Max [s] | Relative |
|---|---|---|---|---|
Cairo-vm (Rust, Cairo 1) |
4.620 ± 0.018 | 4.578 | 4.640 | 2.77 ± 0.04 |
cairo-native (embedded AOT) |
1.669 ± 0.021 | 1.637 | 1.703 | 1.00 |
cairo-native (embedded JIT using LLVM's ORC Engine) |
1.742 ± 0.012 | 1.722 | 1.759 | 1.04 ± 0.01 |
Benchmark for program linear_search
Open benchmarks
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
Cairo-vm (Rust, Cairo 1) |
586.6 ± 6.7 | 576.7 | 598.7 | 1.00 |
cairo-native (embedded AOT) |
1710.9 ± 19.6 | 1687.3 | 1750.0 | 2.92 ± 0.05 |
cairo-native (embedded JIT using LLVM's ORC Engine) |
1735.2 ± 13.5 | 1712.0 | 1755.0 | 2.96 ± 0.04 |
Benchmark for program logistic_map
Open benchmarks
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
Cairo-vm (Rust, Cairo 1) |
498.0 ± 9.4 | 489.5 | 521.6 | 1.00 |
cairo-native (embedded AOT) |
1683.4 ± 13.5 | 1666.5 | 1707.4 | 3.38 ± 0.07 |
cairo-native (embedded JIT using LLVM's ORC Engine) |
1701.8 ± 7.7 | 1690.7 | 1710.9 | 3.42 ± 0.07 |
Benchmark results Main vs HEAD.Base
Head
Base
Head
Base
Head
Base
Head
Base
Head
Base
Head
|
orizi
requested changes
Aug 30, 2026
orizi
left a comment
Collaborator
There was a problem hiding this comment.
@orizi reviewed all commit messages and made 2 comments.
Reviewable status: 0 of 4 files reviewed, 2 unresolved discussions (waiting on TomerStarkware).
src/types/circuit.rs line 69 at r1 (raw file):
Ok(IntegerType::new(context, 64).into()) } // The VM's failure guarantee holds pointers into the builtin segments. There
overdoc
tests/tests/circuit.rs line 329 at r1 (raw file):
} /// Checks that on a failing circuit evaluation, native fills the failure
overdoc
orizi
requested changes
Aug 30, 2026
orizi
left a comment
Collaborator
There was a problem hiding this comment.
@orizi made 1 comment.
Reviewable status: 0 of 4 files reviewed, 3 unresolved discussions (waiting on TomerStarkware).
src/libfuncs/circuit.rs line 427 at r1 (raw file):
))?; // The failure guarantee carries the nullifier and modulus limbs that
overdoc
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.
The bug
eval_circuit's failure branch andcircuit_failure_guarantee_verifyreturnedllvm::undeffor the failure guarantee. The corelib walk that destructs it (u96_limbs_less_than_guarantee_verify) extracts the guarantee's limbs and branches on their comparison — so native branched on poison, while the VM branches on the real(nullifier, modulus)values its hint computes.The walk stops at the first limb pair (most significant first) where the nullifier and modulus differ. In contracts (compiled with optimizations, so every arm carries a
redeposit_gas), the stop position determines how much gas is refunded on the failure path. Native's poison walk folds to a fixed arm, so after any failingcircuit_inverse— e.g. inverting 0, reachable by any modular-arithmetic contract with attacker-controlled input — its remaining gas diverges from the VM: 380 gas for the BLS12-381 modulus, 90 for secp256k1. Around the gas-exhaustion boundary this escalates to a hard divergence: the VM completes the transaction where native reverts with out-of-gas. Branching on poison is also UB regardless of the observed symptom.The fix
CircuitFailureGuaranteewas a ZST in native (the VM's guarantee holds pointers into the builtin segments, which native doesn't have), so it couldn't carry anything. It's now a real value holding the nullifier and modulus limbs:build_gate_evaluationforwards the failing inverse gate's gcd (already computed by the egcd runtime call) to the error block.eval_circuit's error branch materializesnullifier = modulus / gcd(input, modulus)— exactly the value the VM's hint computes (0 < nullifier < modulus,input · nullifier ≡ 0 (mod modulus)) — and stores{nullifier, modulus}in the guarantee.circuit_failure_guarantee_verifyunpacks the carried limbs into itsU96LimbsLtGuarantee<4>output instead of returning undef.No verification is performed — the values exist solely because the destruct walk's branches consume them.
The test
test_data/programs/circuit_failure_guarantee.cairodestructs the guarantee manually (the corelib-private walk API is importable in single-file programs) and returns the walk length as a first-class value, compared VM-vs-native. Two moduli with different expected lengths (secp256k1 → 2,[7,0,0,0]→ 4) ensure any walk not steered by the actual limb values fails at least one case. Verified to fail with a return-value mismatch when the fix is reverted.Note the existing
test_circuit_failcouldn't catch this: its[0,0,0,0]modulus panics attry_into().unwrap()before ever reaching the guarantee path, and the precompiled test programs are built withOptimizations::Disabled, which emits noredeposit_gas, making remaining gas path-independent.🤖 Generated with Claude Code
This change is