|
1 | | -# zkvm-merkle-lean-verified |
| 1 | +# Current status |
2 | 2 |
|
3 | | -A small end-to-end prototype pipeline for verifying zkVM-related Rust code in Lean 4: |
| 3 | +Build a first end-to-end prototype pipeline: |
4 | 4 |
|
5 | | -**Rust → hax extraction → Lean 4 specs/proofs (WIP)** |
| 5 | +Rust → hax extraction → Lean 4 → (specification / proof) |
6 | 6 |
|
7 | | -This repo is an early-stage experiment aimed at establishing a practical workflow to |
8 | | -verify real-world Rust components used in zkVM / zkEVM stacks. |
| 7 | +Target: “representative zkVM-related code” with a clear, simple spec and a tractable first proof milestone. |
9 | 8 |
|
10 | | -## Scope (Phase 1) |
| 9 | +## Choosing an initial verification target |
11 | 10 |
|
12 | | -We extracted a simplified, representative Merkle-path root computation derived from |
13 | | -RISC Zero's `merkle.rs` (receipt Merkle inclusion logic). |
| 11 | +Source: [Risk0 merkle.rs](https://github.com/risc0/risc0/blob/2e73cb82cadfbad9190b2b34f124481c9b57d371/risc0/zkvm/src/receipt/merkle.rs) |
14 | 12 |
|
15 | | -Instead of extracting the full RISC0 codebase, we created a small Rust crate with an |
16 | | -*extract-friendly interface*: |
| 13 | +We selected Merkle root recomputation / inclusion verification (the root() and verify() logic) as the first candidate. |
17 | 14 |
|
18 | | -- Avoids `dyn Trait` (`&dyn HashFn`) by passing `hash_pair` as a function argument |
19 | | -- Avoids `anyhow::Result` by providing a boolean-style verifier |
20 | | -- Keeps the algorithm identical to the original Merkle-path accumulation logic |
| 15 | +```rust |
| 16 | +impl MerkleProof { |
| 17 | + /// Verify the Merkle inclusion proof against the given leaf and root. |
| 18 | + pub fn verify( |
| 19 | + &self, |
| 20 | + leaf: &Digest, |
| 21 | + root: &Digest, |
| 22 | + hashfn: &dyn HashFn<BabyBear>, |
| 23 | + ) -> Result<()> { |
| 24 | + ensure!( |
| 25 | + self.root(leaf, hashfn) == *root, |
| 26 | + "merkle proof verify failed" |
| 27 | + ); |
| 28 | + Ok(()) |
| 29 | + } |
21 | 30 |
|
22 | | -## Repository layout |
| 31 | + /// Calculate the root of this branch by iteratively hashing, starting from the leaf. |
| 32 | + pub fn root(&self, leaf: &Digest, hashfn: &dyn HashFn<BabyBear>) -> Digest { |
| 33 | + let mut cur = *leaf; |
| 34 | + let mut cur_index = self.index; |
| 35 | + for sibling in &self.digests { |
| 36 | + cur = if cur_index & 1 == 0 { |
| 37 | + *hashfn.hash_pair(&cur, sibling) |
| 38 | + } else { |
| 39 | + *hashfn.hash_pair(sibling, &cur) |
| 40 | + }; |
| 41 | + cur_index >>= 1; |
| 42 | + } |
| 43 | + cur |
| 44 | + } |
| 45 | +} |
23 | 46 |
|
24 | | -- `rust/merkle_root_rs/` |
25 | | - Minimal Rust crate containing: |
26 | | - - `merkle_root_from_path` |
27 | | - - `merkle_verify_from_path` |
28 | | - plus Rust unit tests. |
| 47 | +``` |
29 | 48 |
|
30 | | -- `scripts/extract.sh` |
31 | | - Runs: `cargo hax into lean` and copies the generated Lean output into the Lean project. |
| 49 | +### Why this is a strong first candidate |
32 | 50 |
|
33 | | -- `lean/` |
34 | | - Lean 4 project (Lake). |
35 | | - - `lean/MerkleRootLean/Extracted/` contains the hax-generated Lean code. |
36 | | - - `lean/HaxLib/` contains the vendored hax Lean prelude (required by the extracted code). |
| 51 | +- Actually used in both RISC0 and Jolt (commitments to memory/trace pages) |
| 52 | +- Elementary specification: "root = fold over path" |
| 53 | +- Possible to prove the logic's correctness without cryptographic assumptions about the hash |
| 54 | +- Convenient to link with the "Oracle" concept from ArkLib: hash compression can be an oracle |
37 | 55 |
|
38 | | -## Current status |
| 56 | +## Adapt the Rust code for extraction |
39 | 57 |
|
40 | | -- ✅ Rust code builds and tests pass. |
41 | | -- ✅ hax extraction runs and produces Lean code. |
42 | | -- ⚠️ Lean compilation of the extracted code is currently **WIP**: |
43 | | - the extracted Lean code references `Core.*` identifiers (e.g. `Core.Cmp.PartialEq`) |
44 | | - that are not resolved in the current Lean environment. |
45 | | -- ⚠️ There are version-compatibility constraints between the hax Lean prelude and the Lean toolchain. |
46 | | - The toolchain has been adjusted locally, but additional alignment work is required for CI. |
| 58 | +Instead of extracting the entire RISC0 codebase, we created a small Rust crate that preserves the algorithm but uses an extraction-friendly interface. |
47 | 59 |
|
48 | | -## Next steps (Phase 2 / TODO) |
| 60 | +Simplifications performed: |
49 | 61 |
|
50 | | -1. **Fix Lean compilation of extracted code** |
51 | | - - Provide/enable the missing `Core.*` namespaces expected by hax output |
52 | | - - Or adjust hax backend configuration to emit Lean code targeting available libraries |
| 62 | +- Removed Trait Dependencies |
| 63 | +- Eliminated Result Type and Error Handling |
| 64 | +- Flattened Struct into Function Parameters |
| 65 | +- Removed Generic Type Parameters |
| 66 | +- Added Toy Hash for Testing |
53 | 67 |
|
54 | | -2. **Add a Lean specification for Merkle root** |
55 | | - - A pure functional spec (e.g. fold over the authentication path) |
56 | | - - Prove equivalence between the extracted implementation and the spec |
| 68 | +This is acceptable for formal verification prototyping: we preserved the algorithmic core and created a minimal extraction target. |
57 | 69 |
|
58 | | -3. **Re-introduce ArkLib (optional)** |
59 | | - - Use ArkLib as a reference spec library for crypto primitives/protocol structure |
60 | | - - Model `hash_pair` as an abstract oracle function (later: oracle computations) |
| 70 | +Example Rust code (core function): |
61 | 71 |
|
62 | | -4. **Scale up toward real-world components** |
63 | | - - Identify a practical path toward verifying extracted code from zkVMs (Jolt, RISC Zero). |
| 72 | +```rust |
64 | 73 |
|
65 | | -## How to run locally |
| 74 | +// This is a "representative zkVM code": the algorithm is identical, |
| 75 | +// the interface has been adapted for verification/extraction. |
| 76 | + |
| 77 | +#[derive(Clone, Copy, PartialEq, Eq, Debug)] |
| 78 | +pub struct Digest(pub [u32; 8]); |
| 79 | + |
| 80 | +pub fn merkle_root_from_path( |
| 81 | + leaf: Digest, |
| 82 | + index: u32, |
| 83 | + digests: &[Digest], |
| 84 | + hash_pair: fn(&Digest, &Digest) -> Digest, |
| 85 | +) -> Digest { |
| 86 | + let mut cur = leaf; |
| 87 | + let mut cur_index = index; |
| 88 | + for sibling in digests { |
| 89 | + cur = if cur_index & 1 == 0 { |
| 90 | + hash_pair(&cur, sibling) |
| 91 | + } else { |
| 92 | + hash_pair(sibling, &cur) |
| 93 | + }; |
| 94 | + cur_index >>= 1; |
| 95 | + } |
| 96 | + cur |
| 97 | +} |
| 98 | + |
| 99 | +pub fn merkle_verify_from_path( |
| 100 | + leaf: Digest, |
| 101 | + index: u32, |
| 102 | + digests: &[Digest], |
| 103 | + expected_root: Digest, |
| 104 | + hash_pair: fn(&Digest, &Digest) -> Digest, |
| 105 | +) -> bool { |
| 106 | + merkle_root_from_path(leaf, index, digests, hash_pair) == expected_root |
| 107 | +} |
| 108 | + |
| 109 | +// For `сargo test` to pass and provide a basic sanity check, we implement a 'toy hash' function: |
| 110 | + |
| 111 | +#[cfg(test)] |
| 112 | +mod tests { |
| 113 | + use super::*; |
| 114 | + |
| 115 | + fn toy_hash_pair(a: &Digest, b: &Digest) -> Digest { |
| 116 | + let mut out = [0u32; 8]; |
| 117 | + for i in 0..8 { |
| 118 | + out[i] = a.0[i].wrapping_add(b.0[i]) ^ 0x9e3779b9; |
| 119 | + } |
| 120 | + Digest(out) |
| 121 | + } |
| 122 | + |
| 123 | + #[test] |
| 124 | + fn root_and_verify_agree() { |
| 125 | + let leaf = Digest([1,2,3,4,5,6,7,8]); |
| 126 | + let sib1 = Digest([9,10,11,12,13,14,15,16]); |
| 127 | + let sib2 = Digest([17,18,19,20,21,22,23,24]); |
| 128 | + let path = vec![sib1, sib2]; |
| 129 | + |
| 130 | + let root = merkle_root_from_path(leaf, 3, &path, toy_hash_pair); |
| 131 | + assert!(merkle_verify_from_path(leaf, 3, &path, root, toy_hash_pair)); |
| 132 | + } |
| 133 | +} |
| 134 | + |
| 135 | +``` |
| 136 | + |
| 137 | +We extracted the essence of the Merkle proof algorithm while removing RISC0-specific implementation details. |
| 138 | + |
| 139 | +## Extraction to Lean and resolving Lean backend gaps |
| 140 | + |
| 141 | +We successfully run extraction with: `cargo hax into lean` |
| 142 | + |
| 143 | +**Core issue encountered** |
| 144 | + |
| 145 | +The extracted Lean code referenced parts of the modeled Rust core library that were missing or incomplete in the Lean prelude, specifically: |
| 146 | + |
| 147 | +- Core.Cmp (equality traits / operations) |
| 148 | +- Core.Iter.Traits.* (iterator-based loops: into_iter + fold) |
| 149 | +- generated trait boilerplate with AssociatedTypes |
| 150 | + |
| 151 | +**Resolution** |
| 152 | + |
| 153 | +We asked the hax maintainers Source: [Zulip](https://hacspec.zulipchat.com/#narrow/channel/269544-general/topic/hax.20.2B.20lean.20example/with/561950534) |
| 154 | + |
| 155 | +Following guidance from hax maintainers (Zulip), we implemented a local compatibility layer by extending the vendored Hax Lean core model (Hax.Core) with minimal stubs matching the shapes expected by the extracted output. This enabled the extracted file to typecheck. |
| 156 | + |
| 157 | +Key result: the extracted module now builds in our project: `lake build MerkleRootLean.Extracted.Merkle_root_rs` |
| 158 | + |
| 159 | +## First proven theorems in Lean (Proof.lean) |
| 160 | + |
| 161 | +**Theorem 1: `merkle_verify_is_pure_eq`** |
| 162 | + |
| 163 | +Statement (informal): the extracted verification function is definitionally just: |
| 164 | + |
| 165 | +- compute the Merkle root from the path, then |
| 166 | +- compare it to the expected root. |
| 167 | + |
| 168 | +This is important because it confirms the verification function contains no hidden behavior besides recomputation + comparison. |
| 169 | + |
| 170 | +**Theorem 2: `merkle_verify_of_root_ok_is_true` (conditional acceptance lemma)** |
| 171 | + |
| 172 | +Proves basic soundness of the algorithm: |
| 173 | +- If we compute a root `r` via `merkle_root_from_path` |
| 174 | +- And then verify the same root `r` via `merkle_verify_from_path` |
| 175 | +- The result will always be `true` |
| 176 | + |
| 177 | +Because the extracted code lives in the RustM monad (with ok/fail/div), the appropriate “acceptance” statement is conditional: |
| 178 | + |
| 179 | +If merkle_root_from_path ... = RustM.ok r, then verifying with expected_root = r yields RustM.ok true. |
| 180 | + |
| 181 | +Lean proof file compiles successfully |
| 182 | + |
| 183 | +`lake build MerkleRootLean.Proof` |
| 184 | + |
| 185 | +(Proof uses simp plus a simp-lemma for reflexivity of the modeled equality.) |
| 186 | + |
| 187 | +```lean |
| 188 | +import MerkleRootLean.Extracted.Merkle_root_rs |
| 189 | +
|
| 190 | +-- The theorems below are not crypto-security statements; they are |
| 191 | +-- machine-checked properties of the Rust-extracted code (in the current model). |
| 192 | +
|
| 193 | +/-- |
| 194 | +`verify` is definitionally "compute root and compare with expected_root". |
| 195 | +-/ |
| 196 | +theorem merkle_verify_is_pure_eq |
| 197 | + (leaf : Merkle_root_rs.Digest) |
| 198 | + (index : u32) |
| 199 | + (digests : RustSlice Merkle_root_rs.Digest) |
| 200 | + (expected_root : Merkle_root_rs.Digest) |
| 201 | + (hash_pair : |
| 202 | + Merkle_root_rs.Digest → Merkle_root_rs.Digest → RustM Merkle_root_rs.Digest) : |
| 203 | + Merkle_root_rs.merkle_verify_from_path leaf index digests expected_root hash_pair |
| 204 | + = |
| 205 | + (do |
| 206 | + let r ← Merkle_root_rs.merkle_root_from_path leaf index digests hash_pair |
| 207 | + Core.Cmp.PartialEq.eq Merkle_root_rs.Digest Merkle_root_rs.Digest r expected_root) := by |
| 208 | + -- just unfolding the definition is enough |
| 209 | + simp [Merkle_root_rs.merkle_verify_from_path] |
| 210 | +
|
| 211 | +/-- |
| 212 | +Conditional "acceptance" lemma: |
| 213 | +if `merkle_root_from_path ...` evaluates to `ok r`, then verifying with `expected_root = r` |
| 214 | +evaluates to `ok true`. |
| 215 | +
|
| 216 | +This avoids having to prove determinism of re-running `merkle_root_from_path`. |
| 217 | +-/ |
| 218 | +theorem merkle_verify_of_root_ok_is_true |
| 219 | + (leaf : Merkle_root_rs.Digest) |
| 220 | + (index : u32) |
| 221 | + (digests : RustSlice Merkle_root_rs.Digest) |
| 222 | + (hash_pair : |
| 223 | + Merkle_root_rs.Digest → Merkle_root_rs.Digest → RustM Merkle_root_rs.Digest) |
| 224 | + (r : Merkle_root_rs.Digest) |
| 225 | + (hroot : |
| 226 | + Merkle_root_rs.merkle_root_from_path leaf index digests hash_pair = RustM.ok r) : |
| 227 | + Merkle_root_rs.merkle_verify_from_path leaf index digests r hash_pair = RustM.ok true := by |
| 228 | + simp [Merkle_root_rs.merkle_verify_from_path, hroot] |
| 229 | +
|
| 230 | +``` |
| 231 | + |
| 232 | +## Notes from hax maintainers (Zulip) |
| 233 | + |
| 234 | +Source (Zulip thread): |
| 235 | +https://hacspec.zulipchat.com/#narrow/channel/269544-general/topic/hax.20.2B.20lean.20example/with/561950534 |
| 236 | + |
| 237 | +Key points from maintainers: |
| 238 | + |
| 239 | +- Missing pieces of Core.* in Lean are expected right now; work is ongoing. |
| 240 | +- Preferred workaround currently: define missing Core.* locally or patch extracted output. |
| 241 | +- AssociatedTypes generation is intended. |
| 242 | +- They are moving toward a new methodology: core models written in Rust and extracted to Lean, meaning hand-written Lean core models will likely be replaced soon (Lean-only PRs for core stubs are not a priority upstream). |
| 243 | + |
| 244 | +## Current work summary (Stage 1 / Recon) |
| 245 | + |
| 246 | +What we have: |
| 247 | + |
| 248 | +- A small reproducible repo demonstrating Rust → hax → Lean extraction for a zkVM-relevant component. |
| 249 | +- A local Lean compatibility layer enabling extracted code to typecheck (addressing missing Core.Cmp and Core.Iter shapes). |
| 250 | +- Two basic machine-checked theorems about the extracted verification logic. |
| 251 | + |
| 252 | +## Next steps (Stage 2 preparation) |
| 253 | + |
| 254 | +- Replace hand-written Lean core stubs with the recommended approach: |
| 255 | + write minimal Rust “core models” (traits/APIs needed: equality + iteration) and extract them via hax. |
| 256 | +- Strengthen the Merkle specification: |
| 257 | + model the Merkle root computation as a list fold spec and prove equivalence to the extracted implementation (once iterator semantics is modeled, not stubbed). |
| 258 | +- Re-introduce ArkLib / CompPoly integration: |
| 259 | + treat hash_pair as an oracle (ArkLib-style) and connect extracted code to higher-level specs when feasible. |
66 | 260 |
|
67 | | -### Rust |
68 | | -```bash |
69 | | -cd rust/merkle_root_rs |
70 | | -cargo test |
71 | 261 |
|
0 commit comments