|
| 1 | +# Horner algorithm verification (WIP) using CompPoly |
| 2 | + |
| 3 | +This folder documents an ongoing experiment toward an end-to-end verification workflow: |
| 4 | + |
| 5 | +**Rust implementation → hax extraction → CompPoly specification → Lean proofs** |
| 6 | + |
| 7 | +This repository follows a “one repo, multiple PoCs” approach. The Horner PoC currently lives in: |
| 8 | +- rust/horner_eval_rs/ — Rust implementation to be extracted with hax |
| 9 | +- lean-compoly/ — Lean project intended to host CompPoly specs and (eventually) proofs for the extracted code |
| 10 | + |
| 11 | +Status note: this work is **in progress**. At the moment, the hax-extracted Horner code does not compile in the same Lean project as CompPoly due to Lean toolchain version pinning across libraries. The primary success so far is that we raised issues and coordinated with maintainers, who agreed on a unified Lean version. Once the upgrades land, the pipeline should become feasible without splitting projects. |
| 12 | + |
| 13 | + |
| 14 | +## Horner Evaluation |
| 15 | + |
| 16 | +Many zk proof systems (including Plonky3-based stacks) rely heavily on polynomial arithmetic. A minimal but representative component is **univariate polynomial evaluation**. |
| 17 | + |
| 18 | +Given coefficients [c0, c1, c2, …], Horner’s method computes: |
| 19 | +p(x)=c0+x(c1+x(c2+… )) |
| 20 | + |
| 21 | +We aim to verify that the Rust implementation of Horner evaluation corresponds to a mathematical specification expressed using **CompPoly**. |
| 22 | + |
| 23 | +**Reference (algorithmic origin)** |
| 24 | + |
| 25 | +We use Plonky3-recursion as the [Plonky3-recursion](https://github.com/Plonky3/Plonky3-recursion/blob/d3ccaf73d5f707b4a2018ac65ece55d329fee934/recursion/src/pcs/fri/verifier.rs#L98). |
| 26 | + |
| 27 | + |
| 28 | +```Rust |
| 29 | +fn evaluate_polynomial<EF: Field>( |
| 30 | + builder: &mut CircuitBuilder<EF>, |
| 31 | + coefficients: &[Target], |
| 32 | + point: Target, |
| 33 | +) -> Target { |
| 34 | + let mut result = coefficients[coefficients.len() - 1]; |
| 35 | + for &coeff in coefficients.iter().rev().skip(1) { |
| 36 | + result = builder.mul(result, point); |
| 37 | + result = builder.add(result, coeff); |
| 38 | + } |
| 39 | + result |
| 40 | +} |
| 41 | +``` |
| 42 | + |
| 43 | +This is Horner evaluation, but implemented over a circuit builder abstraction. For a first PoC we extract a simpler arithmetic-only version. |
| 44 | + |
| 45 | +## Rust side (implementation + extraction target) |
| 46 | + |
| 47 | +**Create an extraction-friendly Rust crate** |
| 48 | + |
| 49 | +```bash |
| 50 | +cd zkvm-merkle-lean-verified/rust |
| 51 | +cargo new --lib horner_eval_rs |
| 52 | +cd horner_eval_rs |
| 53 | +cargo add --target 'cfg(hax)' --git https://github.com/hacspec/hax hax-lib |
| 54 | + |
| 55 | +``` |
| 56 | + |
| 57 | +**Extraction-friendly Horner implementation** |
| 58 | + |
| 59 | +Because the hax Lean backend is still evolving (and iterator support is limited), we avoid Rust iterators and write the loop using while. This keeps the algorithm identical but reduces extraction complexity. |
| 60 | + |
| 61 | +```Rust |
| 62 | +pub fn horner_eval_i64(coeffs: &[i64], x: i64) -> i64 { |
| 63 | + // p(x) = a0 + a1*x + ... + an*x^n |
| 64 | + // Horner: (((an*x + a_{n-1})*x + ...)*x + a0) |
| 65 | + if coeffs.is_empty() { |
| 66 | + return 0; |
| 67 | + } |
| 68 | + |
| 69 | + let mut i = coeffs.len(); |
| 70 | + let mut acc = coeffs[i - 1]; |
| 71 | + |
| 72 | + while i > 1 { |
| 73 | + i -= 1; |
| 74 | + acc = acc * x + coeffs[i - 1]; |
| 75 | + } |
| 76 | + acc |
| 77 | +} |
| 78 | + |
| 79 | +#[cfg(test)] |
| 80 | +mod tests { |
| 81 | + use super::*; |
| 82 | + |
| 83 | + #[test] |
| 84 | + fn horner_basic() { |
| 85 | + // p(x) = 3 + 2x + x^2, x=10 => 3 + 20 + 100 = 123 |
| 86 | + let coeffs = [3i64, 2i64, 1i64]; |
| 87 | + assert_eq!(horner_eval_i64(&coeffs, 10), 123); |
| 88 | + } |
| 89 | + |
| 90 | + #[test] |
| 91 | + fn empty_is_zero() { |
| 92 | + let coeffs: [i64; 0] = []; |
| 93 | + assert_eq!(horner_eval_i64(&coeffs, 7), 0); |
| 94 | + } |
| 95 | +} |
| 96 | +``` |
| 97 | + |
| 98 | +**Run hax extraction** |
| 99 | + |
| 100 | +We extract to: |
| 101 | +- lean-compoly/CompPolyEval/Extracted/Horner_eval_rs.lean |
| 102 | + |
| 103 | +For convenience we added a script: |
| 104 | +- scripts/extract_horner_to_compoly.sh |
| 105 | + |
| 106 | +## Lean side (CompPoly specification) |
| 107 | + |
| 108 | +We created a separate Lean project: |
| 109 | + |
| 110 | +- lean-compoly/ |
| 111 | + |
| 112 | +and added CompPoly as a dependency. |
| 113 | + |
| 114 | +**Specification module** |
| 115 | + |
| 116 | +We added: |
| 117 | + |
| 118 | +- lean-compoly/CompPolyEval/Spec.lean |
| 119 | + |
| 120 | +This defines a univariate polynomial model (one variable) over ℤ using CompPoly: |
| 121 | + |
| 122 | +- P := CPoly.CMvPolynomial 1 ℤ |
| 123 | +- X0 as the variable X in one dimension |
| 124 | +- polyOfCoeffs : List ℤ → P building the polynomial from coefficients |
| 125 | +- evalAt : ℤ → P → ℤ using CPoly.CMvPolynomial.eval |
| 126 | + |
| 127 | +This enables a clean mathematical statement of Horner correctness in Lean once the extracted code can be compiled in the same environment. |
| 128 | + |
| 129 | +## Proof plan (WIP) |
| 130 | + |
| 131 | +A direct equivalence proof between the extracted Rust function and the CompPoly spec over ℤ will require: |
| 132 | + |
| 133 | +- Panic/exception freedom (or bounds assumptions) for the extracted i64 code, since the hax Lean model uses RustM and overflow-aware operators. |
| 134 | +- A semantic bridge from machine integers (i64) to mathematical integers (ℤ), typically under bounds that rule out overflow. |
| 135 | +- The main functional correctness theorem: extracted Horner evaluation equals evalAt of polyOfCoeffs. |
| 136 | + |
| 137 | +## Current bottleneck: Lean toolchain version mismatch |
| 138 | + |
| 139 | +At the time of writing, the blocker for a single-project end-to-end pipeline is Lean version pinning across libraries: |
| 140 | + |
| 141 | +- hax Lean prelude was pinned to a single Lean version and is sensitive to Lean changes. |
| 142 | +- CompPoly and ArkLib were pinned to different Lean versions. |
| 143 | + |
| 144 | +This prevented compiling the hax-extracted Horner module in the same Lean project as CompPoly. |
| 145 | + |
| 146 | +### Coordination outcome (major progress) |
| 147 | + |
| 148 | +As a result of issues raised during this work, maintainers of hax, CompPoly, and ArkLib agreed to converge on a unified Lean toolchain version: |
| 149 | + |
| 150 | +**Lean 4.26.0** |
| 151 | + |
| 152 | +Once these upgrades land, we expect the end-to-end pipeline: |
| 153 | + |
| 154 | +**Rust → hax → CompPoly spec → proofs** |
| 155 | + |
| 156 | +to become feasible without splitting across incompatible Lean versions. |
| 157 | + |
| 158 | +Reference discussion: hax issue [#1873](https://github.com/cryspen/hax/issues/1873) |
| 159 | + |
| 160 | +## Artifacts in this repository |
| 161 | + |
| 162 | +- rust/horner_eval_rs/ — Rust Horner implementation (tests pass) |
| 163 | +- scripts/extract_horner_to_compoly.sh — extraction helper |
| 164 | +- lean-compoly/CompPolyEval/Spec.lean — CompPoly-based specification (typechecks) |
| 165 | +- lean-compoly/CompPolyEval/Extracted/Horner_eval_rs.lean — hax-extracted Lean code (generated; compilation depends on toolchain alignment) |
| 166 | + |
| 167 | +## Next steps |
| 168 | + |
| 169 | +- Update dependencies once hax/CompPoly/ArkLib are on Lean 4.26.0. |
| 170 | +- Compile extracted Horner code and write the first proof obligations: |
| 171 | + - panic-freedom / bounds, |
| 172 | + - correctness w.r.t. CompPoly eval. |
| 173 | +- Add additional normalization lemmas/tactics (simp/grind sets) as needed to support proof automation for Horner and future polynomial components. |
0 commit comments