Formally prove the correctness of Sqrt.sol and Cbrt.sol#511
Open
duncancmt wants to merge 325 commits intodcmt/newton-raphson-optimizationfrom
Open
Formally prove the correctness of Sqrt.sol and Cbrt.sol#511duncancmt wants to merge 325 commits intodcmt/newton-raphson-optimizationfrom
duncancmt wants to merge 325 commits intodcmt/newton-raphson-optimizationfrom
Conversation
2 tasks
🛡️ Immunefi PR ReviewsWe noticed that your project isn't set up for automatic code reviews. If you'd like this PR reviewed by the Immunefi team, you can request it manually using the link below: Once submitted, we'll take care of assigning a reviewer and follow up here. |
Extract _innerSqrt, _karatsubaQuotient, _sqrtCorrection from _sqrt in 512Math.sol. Extend yul_to_lean.py to support multi-return functions (tuple types, __component_N projections). The pipeline now generates 4 separate Lean models instead of one monolithic ~30-binding term, making each sorry sub-lemma target ~2-10 let-bindings. All private sub-functions are inlined by solc → identical bytecode. Fuzz test (1000 runs) confirms the regenerated Lean model matches. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Prove model_innerSqrt_evm_correct by factoring through norm model: - model_innerSqrt_snd_def: residue = x - fst^2 (by rfl) - model_innerSqrt_snd_eq_residue: norm residue = x - natSqrt(x)^2 - model_innerSqrt_evm_correct: both components correct (uses evm_eq_norm) Add helper theorems natSqrt_lt_2_128 and natSqrt_ge_2_127 for bounds. Remaining sorry: model_innerSqrt_evm_eq_norm (EVM ops = norm ops on bounded inputs, ~10 let-bindings via evm_bstep_eq chain). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract `_bstep` as a separate Solidity function so the Lean model generator produces `model_bstep_evm` calls instead of inlining 6 copies of `evmShr(1, evmAdd(r, evmDiv(x, r)))`. This makes each Babylonian step independently provable via `model_bstep_evm_eq_bstep`. Gas unchanged (solc inlines private pure functions): μ 3549→3549. Prove `model_innerSqrt_evm_eq_norm` by: - Chaining 6 `model_bstep_evm_eq_bstep` applications (each gives equality + [2^127, 2^129) bounds for the next step) - Showing the correction `evmSub z6 (evmLt (evmDiv x z6) z6)` matches `normSub z6 (normLt (normDiv x z6) z6)` via `correction_correct` - Showing the residue `evmSub x (evmMul r r)` matches `normSub x (normMul r r)` since r = natSqrt(x) < 2^128 so r^2 < 2^256 Remaining sorry's: 3 (karatsubaQuotient, sqrtCorrection, composition) plus 1 pre-existing normalization proof broken by Lean v4.28 API changes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rewrite the normalization proof to handle Lean 4.28 breaking changes: - Nat.add_mul_mod_self → Nat.add_mul_mod_self_right - Nat.mul_div_mul_left pattern matching changed - Nat.mul_lt_mul → Nat.mul_lt_mul_of_le_of_lt - ring tactic unavailable (Mathlib-only) - set tactic unavailable (Mathlib-only) Key structural fix: avoid `intro` for let-bindings (which creates opaque defs that rw/simp can't penetrate in Lean 4.28). Instead use `show` to inline all let-bindings upfront, then `intro` only for variables that need case-splitting. Case-split on dbl_k = 0 (where evmShr 256 returns 0 since 256 ≥ 256) vs dbl_k > 0 (where evmShr (256 - dbl_k) works normally). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… 2 sorrys Close the Karatsuba quotient EVM bridge (sorry 2/4 → proved): - Handle no-carry case (res < 2^128): n_evm = n_full directly - Handle carry case (res >= 2^128): correction via WORD_MOD = d*qw + rw + 1 - Change hypothesis from res <= 2*natSqrt(...) to res <= 2*r_hi Revise model_sqrtCorrection_evm_correct spec to raw EVM bridge form: - Result = r_hi*2^128 + r_lo - cmp (where cmp is 257-bit comparison) - Add hypotheses: r_lo <= 2^128, rem < 2*r_hi, hedge condition - Scaffold EVM simplification (constant folding, all ops reduced to Nat) Add helper lemmas: - mul_mod_sq: (a*n) % (n*n) = (a%n)*n - mul_pow128_mod_word: (a*2^128) % 2^256 = (a%2^128)*2^128 - div_of_mul_add / mod_of_mul_add: Euclidean division after recomposition Remaining: 2 sorrys (sqrtCorrection comparison logic, composition proof) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
4-way case split on rem/2^128 ∈ {0,1} × r_lo/2^128 ∈ {0,1}:
- (0,0): comparisons match directly, no EVM overflow
- (0,1): r_lo=2^128, cmp=1, handle evmAdd overflow via evmSub_evmAdd_eq_of_overflow
- (1,0): cmp=0, rem*2^128 ≥ 2^256 > r_lo^2
- (1,1): contradiction via hedge
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Close the last sorry by proving the EVM model of 512-bit sqrt equals the algebraic sqrt512 function. The proof decomposes into: - evm_composition_eq_karatsubaFloor: composition of the three EVM sub-models (innerSqrt, karatsubaQuotient, sqrtCorrection) equals karatsubaFloor on normalized inputs. Uses the Karatsuba algebraic identity x + q² = r² + rem·H + x_lo_lo via correction_equiv. - karatsubaFloor_lt_word: result fits in 256 bits, via karatsubaFloor_eq_natSqrt and natSqrt upper bound. - Main theorem assembly: unfold model_sqrt512_evm, rewrite the composition to karatsubaFloor, then convert evmShr to division. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace all 21 native_decide calls with decide (using maxRecDepth for deep convergence certificates and Fin 256 enumeration). Fix 6 unused variable/simp warnings. Axiom set now minimal: propext, Classical.choice, Quot.sound. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ex-prove-sqrt-cbrt
The Solidity helper functions in 512Math.sol were renamed: _bstep → _sqrt_babylonianStep _innerSqrt → _sqrt_baseCase _karatsubaQuotient → _sqrt_karatsubaQuotient _sqrtCorrection → _sqrt_correction Update generate_sqrt512_model.py to reference the new Solidity names while preserving the Lean model names (model_bstep, model_innerSqrt, etc.) so downstream proofs remain stable. Fix two proofs in GeneratedSqrt512Spec.lean to accommodate the slightly different generated model (double-AND shift wrapping and reversed operand order in addition from the new compiler output). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tructure - Add formal/sqrt/generate_sqrt_cert.py that generates FiniteCert.lean with SqrtCert (256 octaves) and Sqrt512Cert (fixed-seed certificates for octaves 254/255), replacing the hand-written FiniteCert.lean and inline certificate definitions in GeneratedSqrt512Spec.lean. - Refactor model_innerSqrt_evm_eq_norm: extract shared bstep chain and correction logic into evm_innerSqrt_pair, eliminating ~100 lines of duplicated proof code across the .1/.2 components. - Update CI: sqrt-formal.yml and sqrt512-formal.yml now generate the certificate before building, and sqrt512-formal.yml builds proofs (not just the model evaluator), matching sqrt/cbrt patterns. - Consolidate formal READMEs into a single formal/README.md. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Solc 0.8.33 emits `and(and(1,255),255)` type-cleanup wrappers around shift amounts and may reorder commutative operands in the Yul IR for `_sqrt_babylonianStep`. Update the `model_bstep_eq_bstep` and `model_bstep_evm_eq_bstep` proofs to constant-fold the nested AND back to 1 and handle the `add(div(x,z),z)` vs `add(z,div(x,z))` reordering. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Lake warns about a missing manifest when the SqrtProof dependency is resolved during a fresh CI checkout. Track the file in git so it is present at clone time. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ry folding Extend the Yul-to-Lean model generator to handle the public sqrt(uint512) and osqrtUp(uint512, uint512) wrapper functions: - Add mulmod opcode support (evmMulmod/normMulmod) needed by _mul inlining - Add known_yul_names disambiguation in find_function to distinguish sqrt(uint512) from Sqrt.sqrt(uint256) by checking body references - Add mstore/mload memory folding via lazy mstore_sink collection during inlining and _resolve_mloads post-processing in yul_function_to_model - Add switch statement parsing (switch/case 0/default → ParsedIfBlock with else_body), with ConditionalBlock.else_assignments for Lean rendering - Add conditional mstore detection as a hard error - Suppress mstore inlining warnings when mstore_sink handles them - Update Sqrt512Wrapper to use disjoint memory regions (tmp()=0 for result, x:=0x1080 for input) so mstore/mload pairs can be folded - Refactor osqrtUp to move from() outside the conditional so memory writes are unconditional - Update generate_sqrt512_model.py config for flat_sqrt512 and flat_osqrtUp Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reject all switch forms except exactly `case 0 { ... } default { ... }`:
- Duplicate case 0 or default branches
- default not in last position
- Missing case 0 or default (e.g. default-only, case-only)
- More than 2 branches
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- SqrtWrapperSpec.lean: model_sqrt512_wrapper_evm_correct (0 sorry) Proves the sqrt(uint512) wrapper computes natSqrt by case-splitting on x_hi=0 (bridges to SqrtProof's 256-bit proof via namespace compatibility lemmas) and x_hi>0 (reuses model_sqrt512_evm_correct). - OsqrtUpSpec.lean: model_osqrtUp_evm_correct (1 sorry) x_hi=0 case fully proved (bridges to model_sqrt_up_evm_ceil_u256). Helper lemmas fully proved: mul512_high_word (2^256≡1 mod 2^256-1), gt512_correct (lexicographic 512-bit comparison), add_with_carry. x_hi>0 case remains sorry (requires matching generated model to helpers). - GeneratedSqrt512Spec.lean: fix pre-existing model drift (normAnd(normAnd(1,255),255) folding, evmAdd operand reorder), un-privatize EVM simplification lemmas for reuse. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The auto-generated model_osqrtUp_evm inlines the entire 256-bit sqrtUp into the x_hi=0 branch, making unfold + ite_false produce a proof term too deep for the kernel. The generator needs to emit branch-separated definitions to unblock this proof. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…y writes (WS-I) When `if` or `switch` conditions are constant at parse time: - Constant-false `if`: parse the body (tolerating memory writes) but discard it entirely as dead code - Constant-true `if`: flatten the body into the outer scope as straight-line code - Constant-discriminant `switch`: parse all branches, flatten only the matching branch, discard dead branches This prevents spurious "conditional memory write" errors when mstore appears in provably-dead branches, and correctly folds constant-true conditionals so their assignments become straight-line. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…, switch leave 1. find_exact_function: prefer top-level match before searching nested scopes, fixing regression where an unrelated nested homonym caused ambiguity (introduced by 9ffbc2b) 2. collect_all_functions: check both `functions` and `rejected` dicts for duplicate names, so a rejected-then-valid or valid-then-rejected duplicate pair is caught (pre-existing bug) 3. Nested rejected helpers shadow valid outer helpers: remove the outer name from helper_table when an inner scope rejects the same name, preventing silent fallback to the wrong definition (pre-existing bug) 4. Constant-false leave-bearing switch inlining: process the else-body (case-0 branch) as straight-line code instead of rewriting into a new ParsedIfBlock whose condition semantics are inverted, which caused the non-leave constant-fold path to skip the live branch entirely (pre-existing bug, exposed by commit 0bca759's incomplete constant-fold) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ambiguation Two code-quality improvements, zero behavioral changes: 1. _TokenReader base class: _ReferenceScopeParser and YulParser had 7 byte-for-byte identical methods (_at_end, _peek, _peek_kind, _pop, _expect, _expect_ident, _parse_expr). Extract them into a shared base class so a future expression-syntax change only needs one edit. 2. _disambiguate_by_references: extract the dense 3-level conditional block from find_function into a named helper with a docstring that explains the priority ordering for leaf selection (exclude_known=True), wrapper selection (exclude_known=False), and the partial-parse fallback. Add inline comments on each branch. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…olding Duplicated decision logic for constant-folding if/switch conditions existed independently in _inline_single_call (Site B) and yul_function_to_model (Site C). Extract the shared classification into a single _IfFoldDecision enum + _classify_if_fold function, and refactor both sites to use it. Also adds the previously-missing constant-true handling in yul_function_to_model's non-leave path (Site C), which now flattens then-body as straight-line assignments instead of emitting a ConditionalBlock with a constant-true condition. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The THEN_LIVE case in Site C's non-leave path was flattening body
assignments into the outer scope using the outer var_map/subst, which
let block-local variables escape their scope. This caused three bugs:
1. Block-local variables (e.g. `let usr$tmp` inside `if 1 { ... }`)
leaked into the outer scope, silently producing wrong models
2. Block-local pointer variables used by subsequent mstore went
undetected instead of raising "non-constant address"
3. Block-local binders with names matching generated model functions
could cause naming collisions in Lean emission
Fix by processing the body in branch-local copies of var_map/subst/
const_locals (same pattern as _process_conditional_branch), then
emitting only assignments to variables that existed in the outer scope.
Also add an out-of-scope variable check in _process_assignment_into:
after substitution and renaming, any Var not in emitted_ssa_names is
a reference to a variable from a closed scope.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the branch-copy-plus-filter approach with _lower_live_branch,
a dedicated helper that models two scopes during lowering:
- Outer mutable state (var_map, SSA counters, emitted names, constant
facts): outer writes go through the normal SSA machinery immediately,
so reassigned variables get fresh binders (x_2, not a dropped x).
- Block-local overlay: names introduced by `let` inside the branch
are parked in var_map/subst for visibility to later statements in
the same block, then cleaned up at block exit.
Scope survival is decided by source-level binding identity: a target
already in var_map or subst is an outer write; an unknown target is a
block-local declaration.
Fixes three bugs from the prior approach:
1. Outer writes to SSA-renamed variables were dropped because the
branch-local base name didn't match the outer SSA name.
2. Constant facts from outer writes inside the branch were lost,
breaking downstream mstore address resolution.
3. ELSE_LIVE in the leave path used the same unsafe straight-line
pattern; now shares _lower_live_branch with THEN_LIVE.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The fundamental problem: _lower_live_branch inferred declaration-vs-
assignment from name visibility (s.target in outer_known), which breaks
when a `let` inside a taken branch shadows an outer binding with the
same Yul name.
Add `is_declaration: bool` to PlainAssignment and set it in the parser's
_parse_let method. Propagate through alpha-rename, inline_calls body
processing, and block substitution.
_lower_live_branch is now driven by is_declaration:
- Declaration (True): always block-local. The processed expression
goes into subst for visibility to later statements in the block,
then is removed at block exit. Shadowed outer subst entries are
saved and restored.
- Reassignment (False): writes the outer binding via normal SSA.
Parser constant-fold flattening (Site A) also gains block scoping via
_flatten_scoped_block: when flattening constant-true if or constant-
switch live branches, declarations are substituted away and only
reassignments survive to the outer scope.
Fixes:
- `let x` inside constant-true block no longer overwrites outer x
- Block-local real vars available to later outer writes in same block
- Constant-switch with shadowing let now respects block scope
- switch-with-leave shadowing test now passes (bonus)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
is_declaration marks when a name enters the local scope, but the binding's lifetime extends to block exit. A later `x := 2` after `let x := 1` in the same block must update the local binding, not escape to the outer scope. Both _flatten_scoped_block (parser constant-fold) and _lower_live_branch (model lowerer) now maintain a block_locals set that accumulates names from declarations. Subsequent reassignments to names in that set update the local binding instead of emitting to outer scope / outer SSA. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…g paths Replace ad-hoc scope heuristics with a uniform scope model used in all four lowering paths: bare-block flattening, parser constant-fold flattening, constant-live branch lowering, and ConditionalBlock branch processing. Core principle: is_declaration marks when a name enters a block scope. All subsequent writes to that name within the block must target that local binding. At block exit, the binding dies. Structural changes: 1. _split_branch_scoped replaces _split_branch_assignments. Tracks per-branch lexical scope via branch_locals set: declarations create branch-local bindings, later writes to those names stay branch-local, only outer writes survive. 2. Parser bare-block handler uses sequential is_declaration tracking instead of block_let_vars. Real variable declarations (usr$*) are alpha-renamed to fresh internal names (_blk_N) and emitted, so the model lowerer can use SSA for point-in-time value capture. Compiler temps are substituted as before. 3. _process_conditional_branch uses same scope model: declarations create branch-local subst entries with const_locals tracking for mload address resolution. Only outer writes go through _process_assignment_into. 4. _stmt_targets excludes branch-local declarations from outer assign_counts, fixing temporary reuse across disjoint branches. 5. needs_zero_init detects conditional return writes that require explicit zero-initialization for ConditionalBlock else-outputs. 6. ConditionalBlock post-processing preserves const_locals facts when both branches agree on the same constant value. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
_gensym("blk") produces _blk_N names that are valid Yul identifiers.
User code can legitimately declare variables with the same pattern,
causing "assigned 2 times" errors.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add an `avoid` parameter to _gensym that skips counter values whose generated name appears in the given set. The bare-block handler passes all source identifier tokens (lazily collected via _all_source_names) so generated _blk_N names never collide with user-visible identifiers. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…licit Ite/Project IR nodes Multi-return projections and conditional expressions were encoded as fake Call names (`__component_N_M(...)`, `__ite(cond, t, f)`) decoded by regex at every consumption site. This made the IR fragile — a typo in a regex or a new traversal forgetting to special-case these names would silently miscompile. Add two new Expr node types (Ite, Project) to the union alongside IntLit, Var, Call. Update all 14 traversal functions, 3 creation sites, 5 consumption sites, and ~20 test locations. Semantics are identical; all 263 tests pass unchanged. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…nation
Track expression-statements per-branch (on ParsedIfBlock) instead of
per-function. When constant-folding proves a branch dead during inlining
or model generation, its expr_stmts are silently discarded. Live or
non-constant branches still reject expr_stmts as before.
This enables the transpiler to handle Solidity's `/ 3` pattern where the
compiler emits a divide-by-zero guard (`if iszero(y) { panic() }`) that
becomes dead code when the divisor is a known non-zero constant.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three bugs in a7e98d6 where branch expr_stmts were silently lost: 1. Bare-block scoping dropped ParsedIfBlocks that carried body_expr_stmts but had no outer-scope assignments. 2. _flatten_scoped_block reconstructed ParsedIfBlock without propagating body_expr_stmts / else_body_expr_stmts. 3. Constant-true if and constant-switch parse-time paths did not isolate _expr_stmts, so dead-branch expr_stmts leaked to function level and live-branch nested expr_stmts were lost. Root cause: the save/restore pattern for _expr_stmts was manually duplicated at each call site, making it easy to miss new sites. Fix: introduce _parse_scoped_body() that encapsulates the entire _expect("{") + save/restore + _parse_assignment_loop + _expect("}") pattern. All 6 branch-body parsing sites now use it. The bare-block emit check also considers branch expr_stmts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add assert_never() for exhaustive isinstance checks on Expr/RawStatement unions (_alpha_rename, _validate_expr_shape, _check_calls, _wrap_u256_literals) - Add assert narrowing for else_body before iteration in ELSE_LIVE paths - Fix dict key type in _has_live/_has_dead helpers (int, not str) - Rename shadowed variable (chosen_summary → switch_chosen, a → maybe_a) - Annotate ParsedIfBlock switch normalization locals (if_body, else_body, parsed_condition) to avoid type narrowing mismatch - Annotate rename_subst as dict[str, Expr] to satisfy substitute_expr variance - Extract assertEqual arguments to typed locals in test file to avoid list[Any]/set[Any] under disallow_any_expr Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove the two custom NoReturn helpers and use typing.assert_never at all exhaustive isinstance dispatch sites. This gives mypy a proper exhaustiveness check rather than a hand-rolled TypeError. Also remove the sole type: ignore comment by building a typed list[int] instead of filtering tuple[int | None, ...] in _try_const_eval, and add the missing explicit Var early-return there so the union is fully covered. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
_inline_yul_function was the only sequential statement processor that
did not maintain a substitution map. When an intermediate variable was
assigned a constant (e.g. `let expr_1 := cleanup(3)`) and then passed
to a helper with branch expr_stmts (e.g. `wrapping_div(x, expr_1)`),
the helper saw `Var('expr_1')` instead of `IntLit(3)`, making the
branch condition non-constant and incorrectly rejecting the code.
Add a `const_subst` dict that tracks variables assigned to
compile-time-constant values:
- Before inline_calls: substitute known constants into expressions
- After inline_calls on PlainAssignment: record constant results,
or remove the variable if the new value is non-constant
- After ParsedIfBlock: invalidate conditionally-assigned variables
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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.
No description provided.