Skip to content

Commit 20864f0

Browse files
claudehyperpolymath
authored andcommitted
feat(verify): root-comparing verify path + power-of-two leaf layout (VerifyRoot)
The executable plumbing that merkleRootVerifyHashSound licenses: build a Merkle tree from block hashes and compare roots, instead of per-ref hash checks. New module Ochrance.Filesystem.VerifyRoot (all total): - padToLength / nextPow2Exp / layoutLeaves — pad an arbitrary-length block-hash list to a power-of-two leaf Vect (filling with emptyHash). - verifyByRoot — build the padded tree and compare its root to the expected root. - verifyByRootHash — fully live: decode A2ML Hashes via hashToBytes, then root-verify. - fsBlockHashes / verifySnapshotRoot — the runtime path: verify an FSState against an FSSnapshot's committed rootHash. Engineering, not a proof: accept-on-match soundness is the already-proven merkleRootVerifyHashSound (modulo its named CollisionResistant / DecodeInjective boundaries and the primitive ==). Verified: idris2 0.8.0, --build (--total), 28/28 modules, no warnings. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011z2t8zAxfcCNLJzU7YdpBQ
1 parent 1f1597e commit 20864f0

4 files changed

Lines changed: 102 additions & 3 deletions

File tree

.machine_readable/6a2/STATE.a2ml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ actions = [
6363
"Stage 2 provable core COMPLETE — 2.1 validateManifestSound, 2.2 verifyRefsSound, 2.3 parsePairsRoundtrip; all machine-checked. DEFERRED (architectural decision): wire verify to merkleCorrect — the verify path builds no Merkle tree today",
6464
"Stage 3.1 DONE — pure repair primitive correctness (repairBlockPure factored out; sets/preserves/numBlocks/idempotent machine-checked)",
6565
"Stage 3.2 DONE — whole-manifest repair ⇒ verify (repairThenVerify, module RepairVerify). repairRefsPure folds repairBlockPure; proof = verifyRefsComplete ∘ repairRefsConsistent; GoodRefs precondition + hashRefl hypothesis, both named",
66-
"Live-verifier Merkle redesign SOUNDNESS DONE (merkleRootVerifyHashSound) — root-comparing verifier is sound at the Hash level, modulo named CollisionResistant + DecodeInjective. REMAINING is plumbing only (not a proof): pad arbitrary-length block list to power-of-2 leaf Vect + switch the runtime verify path over. NEXT options: that plumbing, or Stage 4 (discharge CollisionResistant against the real combiner), or harvest the VerifiedSubsystem law",
66+
"Live-verifier Merkle redesign SOUNDNESS DONE (merkleRootVerifyHashSound) — root-comparing verifier is sound at the Hash level, modulo named CollisionResistant + DecodeInjective",
67+
"Live-verifier PLUMBING DONE (module VerifyRoot) — padToLength/nextPow2Exp/layoutLeaves pad to power-of-2 leaf Vect; verifyByRoot/verifyByRootHash build tree + compare roots; fsBlockHashes/verifySnapshotRoot = runtime path vs FSSnapshot.rootHash. Executable, total; accept-on-match soundness = merkleRootVerifyHashSound",
6768
"Stage 2.4 DONE — verify↔Merkle proof-level wiring (rootFaithful/rootVerifySound): the root is a faithful fingerprint of the blocks (binding). REMAINING: connect to the live Hash-based verifyRefsHelper via hex Hash<->HashBytes conversion (2.3-bounded) + power-of-two layout (verify-path change, not a proof)",
6869
"Stage 3.2 (next provable): whole-manifest repairPure ⇒ verifyRefsHelper accepts (distinct-in-range ref-name precondition + isolated Hash-reflexivity hypothesis)",
6970
"Remove Idris-side crypto stubs and build + link libochrance.so into the flow (unblocks the crypto-integrity claim)",

docs/PROOFS.adoc

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,8 +229,11 @@ block-hash vectors that decode and yield equal Merkle roots are equal - carrying
229229
boundaries: `CollisionResistant h` (1.4) and `DecodeInjective dec` (the hex wall,
230230
2.3); stated for an arbitrary `dec`, instantiated at `hashToBytes`.
231231
+
232-
STILL OPEN (plumbing, not a proof): padding an arbitrary-length block list to a
233-
power-of-two leaf `Vect` and switching the runtime verify path over - engineering.
232+
DONE (plumbing): `Ochrance.Filesystem.VerifyRoot` - `padToLength` + `nextPow2Exp` +
233+
`layoutLeaves` pad an arbitrary-length block list to a power-of-two leaf `Vect`;
234+
`verifyByRoot` / `verifyByRootHash` build the tree and compare roots; `fsBlockHashes`
235+
+ `verifySnapshotRoot` are the runtime path against `FSSnapshot.rootHash`. Executable,
236+
total; its accept-on-match soundness is `merkleRootVerifyHashSound`.
234237

235238
. *[DONE — 2.3]* Hex codec structural round-trip. The full
236239
`hexStringToBytes (bytesToHex bs) = Just bs` crosses two primitive walls —
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
||| SPDX-License-Identifier: MPL-2.0
2+
|||
3+
||| Ochrance.Filesystem.VerifyRoot - the root-comparing verify path (executable).
4+
|||
5+
||| This is the *plumbing* that the soundness theorem `merkleRootVerifyHashSound`
6+
||| licenses: pad an arbitrary-length block-hash list to a power-of-two leaf `Vect`,
7+
||| build the Merkle tree, and compare its root to a committed root (e.g.
8+
||| `FSSnapshot.rootHash`). Engineering, not a proof - the *soundness* of accepting on
9+
||| a root match is `Ochrance.Filesystem.VerifyMerkle.merkleRootVerifyHashSound`
10+
||| (modulo its named `CollisionResistant` / `DecodeInjective` boundaries and the
11+
||| primitive `==`). All total.
12+
module Ochrance.Filesystem.VerifyRoot
13+
14+
import Data.Vect
15+
16+
import Ochrance.A2ML.Types
17+
import Ochrance.Filesystem.Types
18+
import Ochrance.Filesystem.Merkle
19+
import Ochrance.Filesystem.VerifyMerkle
20+
21+
%default total
22+
23+
--------------------------------------------------------------------------------
24+
-- Power-of-two leaf layout
25+
--------------------------------------------------------------------------------
26+
27+
||| Pad (or truncate) a list to an exact length, filling any shortfall with `pad`.
28+
||| Structural on the target length; the layout below always calls it with a target
29+
||| >= the list length, so it never truncates in practice.
30+
public export
31+
padToLength : (m : Nat) -> a -> List a -> Vect m a
32+
padToLength Z _ _ = []
33+
padToLength (S k) pad [] = pad :: padToLength k pad []
34+
padToLength (S k) pad (x :: xs) = x :: padToLength k pad xs
35+
36+
||| Smallest exponent `n` with `k <= 2^n`, by doubling (fuelled by `k` for totality).
37+
nextPow2ExpGo : (fuel, n, acc, k : Nat) -> Nat
38+
nextPow2ExpGo Z n _ _ = n
39+
nextPow2ExpGo (S f) n acc k = if k <= acc then n else nextPow2ExpGo f (S n) (acc + acc) k
40+
41+
public export
42+
nextPow2Exp : Nat -> Nat
43+
nextPow2Exp k = nextPow2ExpGo k 0 1 k
44+
45+
||| Lay an arbitrary-length block-hash list out as a power-of-two leaf vector, padding
46+
||| with `emptyHash`. The dependent pair carries the chosen height `n`.
47+
public export
48+
layoutLeaves : List HashBytes -> (n : Nat ** Vect (power 2 n) HashBytes)
49+
layoutLeaves xs = let n = nextPow2Exp (length xs)
50+
in (n ** padToLength (power 2 n) emptyHash xs)
51+
52+
--------------------------------------------------------------------------------
53+
-- Root-comparing verify path
54+
--------------------------------------------------------------------------------
55+
56+
||| Root-based block verification: build the padded Merkle tree from the block hashes
57+
||| and compare its root to the expected (committed) root. `True` iff they match.
58+
public export
59+
verifyByRoot : Combiner -> List HashBytes -> HashBytes -> Bool
60+
verifyByRoot h blocks expectedRoot =
61+
let (n ** leaves) = layoutLeaves blocks
62+
in rootHashWith h (buildMerkleTree {n} leaves) == expectedRoot
63+
64+
||| The fully live version: decode A2ML `Hash`es to bytes (via `hashToBytes`), then
65+
||| root-verify. `Nothing` if any hash is malformed; `Just b` with the match result.
66+
public export
67+
verifyByRootHash : Combiner -> List Hash -> Hash -> Maybe Bool
68+
verifyByRootHash h blocks expected =
69+
do blockBytes <- traverse hashToBytes blocks
70+
expBytes <- hashToBytes expected
71+
pure (verifyByRoot h blockBytes expBytes)
72+
73+
--------------------------------------------------------------------------------
74+
-- Wiring to FSState / FSSnapshot
75+
--------------------------------------------------------------------------------
76+
77+
||| Indices `[0, 1, ..., n-1]`.
78+
upTo : Nat -> List Nat
79+
upTo Z = []
80+
upTo (S k) = upTo k ++ [k]
81+
82+
||| Collect a filesystem's block hashes in index order; `Nothing` if any is absent.
83+
public export
84+
fsBlockHashes : FSState -> Maybe (List Hash)
85+
fsBlockHashes fs = traverse fs.blockHash (upTo fs.numBlocks)
86+
87+
||| The runtime root-verify path: check a filesystem against a snapshot's committed
88+
||| Merkle root. `Nothing` if a block is missing or a hash is malformed; otherwise
89+
||| `Just` the match decision. Soundness: `merkleRootVerifyHashSound`.
90+
public export
91+
verifySnapshotRoot : Combiner -> FSState -> FSSnapshot -> Maybe Bool
92+
verifySnapshotRoot h fs snap =
93+
do blocks <- fsBlockHashes fs
94+
verifyByRootHash h blocks snap.rootHash

ochrance.ipkg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ modules = Ochrance.A2ML.Types
3232
, Ochrance.Filesystem.Verify
3333
, Ochrance.Filesystem.VerifyProof
3434
, Ochrance.Filesystem.VerifyMerkle
35+
, Ochrance.Filesystem.VerifyRoot
3536
, Ochrance.Filesystem.Repair
3637
, Ochrance.Filesystem.RepairProof
3738
, Ochrance.Filesystem.RepairVerify

0 commit comments

Comments
 (0)