Skip to content

docs: RFC 001 — formal verification of ICS23 - #421

Open
zmanian wants to merge 67 commits into
cosmos:masterfrom
zmanian:rfc/formal-verification
Open

docs: RFC 001 — formal verification of ICS23#421
zmanian wants to merge 67 commits into
cosmos:masterfrom
zmanian:rfc/formal-verification

Conversation

@zmanian

@zmanian zmanian commented Jun 10, 2026

Copy link
Copy Markdown

Summary

This PR began as an RFC proposing a formal verification program for the ICS23 verifier and now carries the verification itself: a mechanized Lean 4 model of the verifier with machine-checked soundness theorems, Kani panic/overflow harnesses, an executable model with concrete SHA-256, and a set of findings surfaced by the work.

The verifier is the security boundary of IBC state verification: every byte of a CommitmentProof is attacker-controlled, and a soundness bug means forgeable state proofs across IBC. The hand-written verification core is small, purely functional, and has a crisp cryptographic correctness statement — an unusually good target for formal methods.

What is proved (Lean 4, no Mathlib, ~3,800 lines)

All soundness results conclude by constructively exhibiting a hash collision — there are no axioms about the hash functions. The only cryptographic assumption that remains is FixedHash (a fixed-width digest), and even joint leaf injectivity is now a theorem rather than an assumption.

  • Theorem A — existence binding. Two accepted existence proofs for the same key under the same root agree on the value, up to a collision. Proved byte-level (existence_binding_shaped, production-spec shape) and, in the honest-root tree model, as genuine membership in the real tree with no positional-ambiguity arm (membership_sound / membership_sound_tendermint).
  • Theorem B — non-existence soundness. A non-existence proof and an existence proof for the same key cannot both verify without a collision, proved in the honest-root tree model for the two-sided neighbor case (nonexistence_sound_tree / nonexistence_sound_tree_tendermint). The chain: membership_sound makes the absent key a genuine member; ensureLeftNeighbor_spec + neighbor_divergence pin the bracketing proofs to a divergence node N; node_gap_no_member shows no member can sit between maxKey N.left and minKey N.right in a sorted tree.
  • Joint leaf injectivity, discharged from the encoding. Previously a hypothesis (hLInj), now proved for every shipped spec: leafInj_varProto (IAVL/Tendermint, via varint self-delimiting) and leafInj_noPrefix (SMT, via the fixed-32-byte digest split). The reduction is exactly "encoding framing + collision resistance."
  • Implementation safety. Six Kani harnesses (cfg(kani)-gated) over the arithmetic and slice-indexing in the verifier internals — the Dragonberry-class surface — including the i32 prefix bounds, padding offsets, left/right empty-branch slice bounds, and compressed-batch index handling.
  • Executable model. A runnable Lean model with concrete SHA-256 (Sha256.lean, Executable.lean) and a regression corpus, with the non-existence verifier exercised end-to-end on a real tree.

One acknowledged sorry remains, by design: the abstract-root nonexistence_sound. It is not provable as stated — an opaque root carries no tree structure linking byte-order to position (findings F3/F4) — which is exactly why the genuine result is the honest-root nonexistence_sound_tree, mirroring how Theorem A's real deliverable is honest-root membership_sound. CI guards the sorry count at exactly one.

Findings surfaced by the work

  • F1 — i32 prefix-bound overflow in ensure_inner under an adversarial spec with an enormous child_size. Not reachable with the shipped specs; a Kani harness pins the safe precondition.
  • F2 — ternary-spec suffix slice panic. The right empty-branch check guards a single child_size suffix but indexes multiple siblings; latent for any spec with child_order.len() > 2. No shipped store is ternary.
  • F3 — positional ambiguity. A node preimage is accepted by ensure_inner under both a left-child and a right-child reading. Binding against an arbitrary hash therefore rests on preimage resistance, not just collision resistance. Machine-checked that the IAVL ensure_inner_prefix (height/size/version) does not disambiguate it. The honest-root tree model resolves this structurally via split_pins: the verifier's suffix % child_size check pins a node split to exactly the two genuine children.
  • F4 — non-existence soundness requires the store key-sortedness invariant. The verifier links key order to position adjacency but never checks that positions track key order, so soundness is conditional on a store invariant the verifier does not enforce. The Lean theorem makes this precise by carrying a SortedTree hypothesis.
  • F5 — adjacency is hash-structural, not pure lex. "No leaf position strictly between the neighbors" holds only because the left neighbor's leaf is terminal — a fact about the tree's hash structure, reducing to the leaf/inner domain-separation collision, not derivable from the lexicographic order alone.

Repo layout

  • docs/rfc/001-formal-verification.md — the RFC.
  • docs/verification/properties.md — properties, findings, and a traceability matrix.
  • lean/ — the Lean 4 Lake project; lake build checks every proof, and CI runs it.
  • rust/src/kani_proofs.rs — the Kani harnesses.

Approach and alternatives

Model-first: a mechanized Lean model of the verifier with machine-checked soundness theorems, intended to connect to both shipped implementations via a differential oracle, with Kani harnesses and fuzz targets on the Rust crate, and an optional refinement proof of ics23-rs (hax / coq-of-rust) as a stretch goal. Alternatives considered (Verus/Creusot, TLA+/Quint, F* end-to-end, more auditing) are discussed and rejected with reasoning in the RFC.

Discussion wanted

The RFC ends with open questions for maintainers — Lean 4 vs. Rocq, whether the refinement proof is in scope, batch proof sequencing, and whether the model should live in this repo or a sibling.

🤖 Generated with Claude Code

Proposes a mechanized Lean 4 model of the verifier with machine-checked
soundness theorems (existence binding, non-existence soundness, and a
decidable ProofSpec well-formedness certificate), connected to the Go and
Rust implementations via a differential oracle, Kani harnesses, and an
optional refinement proof through hax or coq-of-rust.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Jun 10, 2026

Copy link
Copy Markdown

PR author is not in the allowed authors list.

zmanian and others added 28 commits June 10, 2026 12:41
Begins executing RFC 001. Adds a dependency-free Lean 4 model of the ICS23
existence verifier mirroring rust/src/{ops,verify,api}.rs:

- Types/Ops/Verify/Specs: proto types, hashing ops (abstract hash family,
  no collision-resistance assumption), the existence verifier, and the three
  shipped specs (IAVL/Tendermint/SMT).
- Soundness: a decidable WellFormed predicate capturing the side conditions
  for binding, with machine-checked certificates that all three shipped specs
  satisfy it (Theorem C); the inner-preimage cancellation lemma; and the
  precise statement of Theorem A (existence binding), proof in progress.
- docs/verification/properties.md: Phase 0 property catalogue, modeling
  assumptions, and the regression corpus to encode.

Theorem A is the only sorry; nothing proved depends on it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- NonExist.lean: faithful model of verify_non_existence and its padding/
  neighbor helpers (ensure_{left,right}_most, ensure_left_neighbor,
  {left,right}_branches_are_empty, get_padding, order_from_padding), with
  panic sites modeled as sound rejections.
- Corpus.lean: spec-level invariant violations (A2 domain separation,
  A3 child-size/prefix-window, C malformed specs, depth bounds) encoded as
  machine-checked accept/reject facts closed by decide.

Completes the executable verifier model (existence + non-existence). Theorem A
remains the only sorry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds fully-proved building blocks toward existence binding:
- hashCollision_of: package a witnessed clash.
- applyInner_inj: one inner step is injective in its child up to an explicit
  collision.
- applyPath_sameops_inj: folding a shared op-list over two inputs to the same
  root forces the inputs equal, up to a collision (the inductive backbone).

Theorem A remains the only sorry; the remaining gap is leaf injectivity (A1)
and the differing-path case (positional unambiguity, A3).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
existence_binding_noPrefix_sameshape fully proves Theorem A for the SMT/JMT
leaf shape (NoPrefix length) when two proofs share tree structure: a value-swap
forgery on one key forces a hash collision. Exercises the full chain — root
extraction (verifyExistence_root), path-fold injectivity, leaf-image
cancellation, and NoPrefix leaf-encoding injectivity — with no new sorry.

The general existence_binding remains the only sorry; what's left is the varint
self-delimiting case (length-prefixed specs) and the differing-path case.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Varint.lean: varintDecode + varintDecode_roundtrip prove the varint length
  prefix is self-delimiting (varintEncode_append_inj), giving leaf-encoding
  injectivity (A1) for length-prefixed specs; doLength_varProto_inj.
- Existence.lean: generalize binding to existence_binding_sameshape (any
  injective length op), with corollaries for NoPrefix (SMT/JMT) and VarProto
  (IAVL/Tendermint). The value-swap forgery on a shared tree shape provably
  forces a hash collision for all three shipped leaf shapes.

No new sorry. The general existence_binding's only remaining gap is the
differing-path case (A3).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds machine-checked facts over the SMT inner spec exercising the
non-existence verifier's padding, ordering, and empty-branch placeholder logic
(order_from_padding, is_left_step, ensure_{left,right}_most,
left_branches_are_empty) — the Dragonberry-class surface. Mirrors the Rust
check_empty_branch test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- .github/workflows/lean.yml: build all proofs on lean/ changes and guard that
  exactly one (expected) sorry remains.
- properties.md: proof status and the precise remaining obligations (general
  Theorem A differing-path case, Theorem B tree semantics).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- NonExistSound.lean: precise statement of Theorem B (non-existence soundness)
  staged with sorry, plus proved bytesLt_irrefl and bytesLt_ne (the
  lexicographic ordering facts the neighbor checks rely on).
- Wire into root module; bump CI sorry-guard to the two whitelisted goals
  (general existence_binding, nonexistence_sound).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fully-proved Theorem B infrastructure:
- bytesLt_trans: lexicographic order is transitive.
- verifyNonExistence_{left,right}: extract the neighbor existence proofs and
  the strict ordering of each neighbor key against the absent key.
- verifyNonExistence_neighbors_ordered: a two-sided non-existence proof brackets
  the key strictly (left key < right key) — a verified consistency property.

Leaves the tree-semantics core of nonexistence_sound as the only Theorem B sorry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Sha256.lean: pure-Lean SHA-256, validated against the digests embedded in
  rust/src/ops.rs (food, foobar, empty) via native_decide.
- Executable.lean: concreteHash : HashFn plus end-to-end runs of the verifier —
  honest IAVL proofs (leaf-only and one-inner) verify; value-swap and
  wrong-tree-shape forgeries are refuted by computation.

Makes the abstract model runnable and seeds the Phase 2a differential oracle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Builds a concrete 2-leaf Tendermint tree with SHA-256, proves non-membership of
a key strictly between the leaves, confirms both leaves hash to one root, and
shows the verifier refuses to prove non-membership of an existing key — the
Theorem B property demonstrated by computation (native_decide).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three CBMC-verified harnesses (rust/src/kani_proofs.rs, cfg(kani), invisible to
normal builds; cargo kani reports 3/3 successful):
- ensure_inner prefix-bound i32 arithmetic is overflow-free under well-formed
  bounds (with a noted finding: an adversarial enormous child_size could
  overflow it);
- get_padding's idx*child_size / child_size*(n-1-idx) products are overflow-free;
- left_branches_are_empty slice accesses are always in bounds — the
  Dragonberry-class out-of-range-slice surface.

Adds .github/workflows/kani.yml (official kani action) and declares the kani cfg
in Cargo.toml so normal builds stay warning-free.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Add right_branches_slice_in_bounds_binary harness (4/4 Kani harnesses now
  verified). Covers the suffix slice access for binary specs (all supported
  stores).
- Document two findings surfaced by the work: F1 (i32 prefix-bound overflow for
  a malformed enormous child_size) and F2 (left/right empty-branch asymmetry —
  right_branches_are_empty can index a child_size-long suffix out of bounds for
  ternary+ specs; latent since no supported store is ternary).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Maps every verifier function (ops/verify/api, existence + non-existence) to its
Lean model counterpart and the machine-checked results about it (proved /
in-progress / Kani-verified / corpus-exercised), plus the executable layer and
Kani harnesses. Keeps the verification maintainable as the code evolves.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Working the differing-path case of general existence_binding surfaced a
structural finding, now substantiated by computation in Executable.lean: for the
Tendermint spec, the node preimage [1] || A || B is accepted by ensure_inner
both as a left-child step (child A, sibling B in suffix) and a right-child step
(child B, sibling A in prefix), producing the SAME node hash from DIFFERENT
children. So the spec checks alone do not pin a node's recursive child, and
general binding cannot reduce to collision resistance against an arbitrary hash —
it needs an injective/opaque hash model or the per-store prefix structure the
model abstracts (e.g. IAVL ensure_inner_prefix). Documented as F3; this is the
precise obstacle to closing general existence_binding (the same-shape theorem
sits below it). No evidence of a live exploit on the shipped stores.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Document in the proof that the general theorem is open due to the positional
ambiguity (F3), which is a model-refinement question (injective/opaque hash
model or per-store prefix structure), not a tactic gap.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lve F3

Per the chosen direction (re-include IAVL prefix checks to try to pin node
position): IavlPrefix.lean faithfully models ensure_iavl_prefix / ensure_inner_
prefix / ensure_leaf_prefix (zigzag-varint height/size/version + remaining-byte
count). native_decide witnesses then show the check accepts BOTH readings of the
same node preimage hsv || 0x20 || A || 0x20 || B — left leaves remaining=1, right
leaves remaining=34, and ensure_inner_prefix admits both.

Conclusion: re-including the IAVL prefix checks does not resolve F3; the
positional ambiguity is intrinsic to the prefix/suffix construction for every
shipped spec. General binding rests on preimage resistance, so closing
existence_binding needs an opaque/injective ('symbolic Merkle') hash model or a
weaker 'collision OR preimage' conclusion — a foundational reformulation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tement

The original existence_binding concluded a hash collision outright. Finding F3
(machine-checked, incl. IavlPrefix.lean) shows that is too strong byte-level:
the differing-path case can be a positional ambiguity rather than a collision.
So the conclusion is corrected to the honest, true statement:
  HashCollision H  OR  PositionalAmbiguity s
where PositionalAmbiguity formalizes F3 (two distinct ensureInner-accepted ops
decomposing one node image). The same-shape theorem still yields a collision
outright.

Adds proven scaffolding toward the assembly: IsInnerImage and
applyPath_result_isInnerImage (a non-empty path fold yields an inner-node image,
the component that discharges the length-mismatch case via leaf/inner domain
separation). The assembly induction remains the sorry; the symbolic-Merkle model
(or this disjunction) is the documented path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…hening)

existence_binding_eqlen fully proves (no sorry) binding for two existence proofs
of the same key, same leaf op, and equal path LENGTH but arbitrary differing
inner ops: they yield HashCollision OR PositionalAmbiguity (F3). This strictly
strengthens same-shape binding (which required identical ops) and covers all
equal-depth proofs.

Supporting, all proved: applyPath_eqlen_merge (the equal-length merge
induction), applyInner_image, ensureInner_hash, verifyExistence_inners, plus a
LawfulBEq HashOp instance. The general (arbitrary-length) existence_binding
remains sorry — only the length-mismatch leaf/inner domain-separation case is
left; applyPath_result_isInnerImage is its proven component.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ligations

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
leaf_inner_domain_collision: a leaf preimage (starting with the leaf prefix byte)
and an inner preimage (not) that hash-collide under the same op yield an explicit
HashCollision. This is the key reusable component for the length-mismatch case of
general existence_binding (short proof's leaf hash meets long proof's
intermediate node hash).

With applyPath_result_isInnerImage and applyPath_eqlen_merge, all components of
the general binding proof are now proved; only the root-side inductive assembly
(aligning two differing-length folds from the root) remains as the sorry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ral core)

applyPath_snoc: root-side characterization of a path fold (peel the last op).
applyPath_merge: the structural core of general existence binding — two
spec-conformant paths folding h1,h2 to one root yield collision, F3 ambiguity,
h1=h2, or one input is an inner-node image (length-mismatch). Proved by root-side
strong induction on total path length; because root-side peeling keeps the leaf
inputs fixed, the conclusion is directly about them. Uses applyPath_snoc,
applyInner_image, ensureInner_hash, innerImage_inj, applyPath_result_isInnerImage.

This reduces general existence_binding to leaf-level case analysis (leaf
injectivity for h1=h2; leaf/inner domain separation for the inner-image cases).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
hasPrefix_single_head / not_hasPrefix_single_head: a single-byte prefix [b]
determines (or excludes) the head byte of data. These let the leaf/inner
domain-separation collision fire at the leaf level, the last component for
wiring applyPath_merge into general existence binding.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
existence_binding_sameleaf (no sorry): two existence proofs sharing the same
leaf op but with ARBITRARY differing-length paths, binding one key to two values
under one root, yield HashCollision OR PositionalAmbiguity. This is the strongest
binding result, built on the root-side structural core applyPath_merge +
applyPath_snoc and leaf/inner domain separation (leafHash_innerImage_collision,
hasPrefix head lemmas), with leaf_value_collision and verifyExistence_leaf
helpers.

Instantiated for all three shipped specs with side conditions closed by decide:
existence_binding_{iavl,tendermint,smt}.

The only remaining existence sorry is the fully-general case (different leaf ops
-> leaf-level prefix ambiguity); for a fixed key in a real tree the leaf op is
determined, so the same-leaf theorem covers the honest case.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
decompress_index_no_panic (5/5 Kani harnesses now verified): decompress_exist's
lookup.get(x as usize) is panic-free for any attacker-controlled i32 path index
— a negative/out-of-range index wraps under 'as usize' and get returns None
(mapped to an empty path), so the compressed-batch decode cannot panic on the
index. Closes the compressed-batch item of Phase 3.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
existence_binding_shaped is fully proved (no sorry): for the production-spec
shape (single-byte leaf prefix, shared leaf/inner hash op, min_prefix_length>=1),
two existence proofs binding one key to two values under one root -- with NO
assumption on their leaf ops or paths -- yield the honest three-way disjunction
HashCollision OR PositionalAmbiguity OR LeafAmbiguity.

The two ambiguity arms (F3 and its leaf-level analogue, LeafAmbiguity) are real,
machine-checkable obstructions the abstract-hash model cannot rule out; this is
the honest complete existence theorem. Built on the root-side structural core
applyPath_merge + ensureLeaf_eq (leaves conforming with equal prefix are equal)
+ leafHash_innerImage_collision. Adds LawfulBEq LengthOp.

The Soundness.existence_binding sorry is removed. sorry count 2 -> 1: only
nonexistence_sound (Theorem B) remains, needing the symbolic-Merkle model.
CI sorry guard updated to 1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
applyPath_merge on (existence proof, neighbor proof) is vacuous: two different-
key proofs legitimately diverge, which the model reads as PositionalAmbiguity
(meaningful only for same-key existence binding). Non-existence soundness lives
entirely in the key ordering + neighbor checks, requiring the ordered-tree
position model. Captured as a finding for the Theorem B effort.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
zmanian and others added 30 commits June 10, 2026 19:45
Tree.lean: inductive binary MTree with explicit structural bytes, rootHash
(mirroring the verifier's leaf/inner hashing), TreeMember, and the left/right
child-op reconstruction lemmas (a node's hash is reproduced by applying the
left-child op to the left subtree hash, or the right-child op to the right).

States membership_sound (Theorem A, honest-root form): an existence proof
verifying against root = rootHash t implies genuine membership in t, up to a
collision. Against a real root the F3 readings are genuine left/right children,
so the inner positional ambiguity is resolved by tree structure. Proof to follow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
split_pins is the crux of Option 1: for a binary node pre++lh++rh with
|lh|=|rh|=cs, the verifier's prefix-length bound and suffix.length % cs = 0 check
pin any accepted cs-length child split to EXACTLY the two genuine children (left
or right subtree hash) — there is no straddling third reading. This is the
structural fact that resolves the F3 positional ambiguity against a real root,
proved by length arithmetic + List.append_inj.

membership_sound's induction will apply this at each node. The crux is done.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
WFTree: Tendermint-shaped conformance (node = H ih (pre++lh++rh), inner-spec
hash, nonempty non-leaf-prefixed node prefix, spec leaf op). FixedHash: cs-byte
digests (SHA-256 = 32 = child_size). applyLeaf_len / applyInner_len: outputs are
cs-length — needed to feed split_pins' |child| = cs at each node.

membership_sound now stated with the proper honest-root hypotheses; its
induction (applying split_pins per node) is the remaining piece. The conceptual
crux (F3 resolution, split_pins) is already proved.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Builds the membership-soundness induction (reaches) for honest roots:
- applyLeaf_head, ensureLeaf_self helpers; rootHash_len / applyPath_len.
- reaches LEAF case fully proved: empty path -> leaf-leaf comparison via the
  joint-leaf-injectivity hypothesis (genuine membership or collision); nonempty
  path -> root is an inner image but also a leaf hash -> domain collision.
- reaches NODE case: applies split_pins (the proven F3-resolution crux) to force
  the proof into a genuine left/right child, then recurses. The split_pins
  assembly is the remaining sorry.
- membership_sound wraps reaches (sorry pending reaches).

sorry count 1 -> 3 temporarily (the two Option-1 goals + Theorem B); CI guard
updated. The conceptual crux (split_pins) and leaf case are proved; the node
case is mechanical assembly. This closes the F3 ambiguity against real roots.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
membership_sound is fully proved: an existence proof verifying against root =
rootHash t (a real Tendermint-shaped tree) implies genuine membership in t, up to
a hash collision — with NO ambiguity arm. The F3 positional ambiguity is fully
resolved by split_pins: against a real root, both 'readings' of a node are the
genuine left/right children, so the proof follows actual tree structure.

The core induction (reaches) applies split_pins at each node, recursing into the
real child; the leaf base uses joint-leaf-injectivity (hypothesis) and leaf/inner
domain separation. split_bounds extracts split_pins' bounds from ensure_inner's
i32 arithmetic; rootHash_len/applyPath_len supply the cs-length facts.

This is Option 1 delivered for existence: a strictly stronger, honest result than
the byte-level disjunction. sorry count back to 1 (only nonexistence_sound).
Next: derive Theorem B from membership_sound (sortedness holds by construction in
the tree model).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
membership_sound_tendermint instantiates the honest-root membership theorem for
the Tendermint spec, discharging all structural side conditions by decide and
leaving only FixedHash (32-byte digests) and joint leaf injectivity as the clean
cryptographic assumptions. Documents the Option-1 honest-root Theorem A win.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The joint-leaf-injectivity hypothesis must require both applyLeaf results to be
some r: the unconditional form is false when keys are empty (both sides none, but
keys/values needn't match). Fixed in reaches, membership_sound, and the
Tendermint corollary. membership_sound remains fully proved; sorry count 1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
TreeNonExist.lean: minKey/maxKey/SortedTree (key-sorted BST predicate) and the
BST bounding lemmas — minKey_le_maxKey, member_le_maxKey, minKey_le_member
(a member's key lies within [minKey t, maxKey t]). These are the ordered-tree
reasoning that, with membership_sound, will close non-existence soundness: no
member sits strictly between two adjacent leaves.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
root_gap_no_member: in a sorted node, no member sits strictly between the left
subtree's max and the right subtree's min — the core ordered-tree fact for
non-existence. Built on the bounding lemmas + bytesLt transitivity/irreflexivity.

Insight: ensure_right_most forces each step's suffix length to 0, which is exactly
split_pins' 'right child' case — so the byte-level neighbor checks connect to the
tree structure through split_pins (the same crux that resolved F3 for existence).
The remaining work is the reaches-style lemma 'all-right path reaches maxKey'.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…in leaf)

The byte->tree connection for Theorem B's neighbor structure:
- split_right / split_left: empty suffix forces the right child, full cs-suffix
  forces the left child (specializing split_pins).
- reaches_max: an all-right path (ensure_right_most) reaches maxKey t.
- reaches_min: an all-left path (ensure_left_most) reaches minKey t.
- ensureInner_minle: ensure_inner's lower prefix bound.

These mirror the reaches induction but force a single direction via split_right/
left. With membership_sound + root_gap_no_member, the remaining work is the
ensure_left_neighbor decomposition (navigate to the divergence node, then the
all-right/all-left tails) and the general-node reduction to the root gap.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The BST side of Theorem B is now complete:
- IsSubtree, subtree_sorted, subtree_minKey_le/maxKey_ge.
- member_in_subtree_range: subtree contiguity — a member whose key is in a
  subtree's range is in that subtree (sorted trees occupy contiguous ranges).
- ble_trans/ble_not_gt: <= order helpers.
- node_gap_no_member: for ANY subtree N of a sorted tree, no member sits strictly
  between maxKey(N.left) and minKey(N.right). Generalizes root_gap via contiguity.

With membership_sound + reaches_max/reaches_min, the only remaining piece is the
ensure_left_neighbor decomposition connecting the byte paths to the divergence
subtree N (l = maxKey(N.left), r = minKey(N.right)).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extracts the structured content of a passing ensure_left_neighbor check:
the first divergent ops (topLeft/topRight) after the shared root path form a
left-step, the left remainder is right-most, the right remainder is left-most.
This is the byte-level entry point for connecting the neighbor walk to the
divergence subtree in the honest-root tree model.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ensureRightMost_suffix_nil / ensureLeftMost_suffix_cs: for a binary spec whose
emptyChild sentinel is not a full cs-byte digest (the Tendermint shape,
emptyChild = []), every step of an ensure_right_most / ensure_left_most path is
a genuine pad (empty / full-cs suffix), never an empty-branch placeholder. The
placeholder branch would require emptyChild.length = cs, excluded by hypothesis.

This resolves the subtlety where a placeholder 'right-most' step (suffix length
cs) would split-pin LEFT rather than right — pinning the precise precondition
under which reaches_max/reaches_min apply to the neighbor remainders.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The combined induction connecting the ensure_left_neighbor byte structure to the
honest tree. Two verifying existence proofs whose reversed paths share a root-side
prefix and then diverge as a left-step navigate to a common node N (a subtree):
the left proof reaches maxKey(N.left), the right reaches minKey(N.right), up to a
collision.

- Common root step (eqPS top ops): append-cancellation gives mL = mR; both proofs
  descend into the same child; recurse, lifting the subtree witness via IsSubtree.
- Divergence (isLeftStep): this node is N; split_left/split_right pin the two
  proofs into l and r; the right-most/left-most remainders feed reaches_max /
  reaches_min (suffix shapes supplied by the F4/F5 padding bridges).
- Empty paths / leaf trees collapse to leaf-inner domain or inner-image collisions.

With node_gap_no_member this is everything needed to assemble nonexistence_sound
in the honest-root model.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t model

nonexistence_sound_tree (+ _tendermint wrapper): the honest-root analog of
membership_sound, fully proved with no sorry. For a key-sorted honest tree, a
two-sided non-existence proof and an existence proof for the same key cannot both
verify without a hash collision.

The assembly:
- verifyExistence_navigates: a verifying existence proof folds its leaf hash
  along its path to the root (extracted from membership_sound's body).
- membership_sound makes the absent key a genuine tree member (or collision).
- ensureLeftNeighbor_spec + neighbor_divergence pin the bracketing proofs to a
  divergence node N: left reaches maxKey(N.left), right reaches minKey(N.right).
- node_gap_no_member: in a sorted tree no member sits in that gap; the absent
  key, were it a member, would — contradiction, forcing a collision.

The abstract-root nonexistence_sound stays the one acknowledged sorry by design
(F3/F4: an opaque root has no tree structure linking byte-order to position),
exactly as Theorem A's real result is honest-root membership_sound. Docs and CI
sorry-guard updated to reflect that both theorems are now proved in the tree model.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds do_length_fixed_no_panic: the data.len() casts and to_be/le_bytes extends
for NoPrefix and the Fixed32/64 ops cannot panic. VarProto/Require* are excluded
(anyhow construction blows up CBMC's formula).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ding

leafInj_varProto (LeafInj.lean): for any varProto-framed leaf op, two leaves
hashing to the same value share their (key, value) or exhibit a hash collision —
no axioms. Reduction is varint-self-delimiting + collision resistance: equal
images give either a collision on the leaf hash op (differing preimages) or equal
preimages, which cancel the fixed prefix; varintEncode_append_inj + append_inj
then peel the two length-framed pieces, leaving H_phk k1 = H_phk k2 and
H_phv v1 = H_phv v2, each an equality or a collision on the prehash op.

Instantiated axiom-free as leafInj_tendermint and leafInj_iavl (identical leaf
ops). The Tendermint wrappers membership_sound_tendermint and
nonexistence_sound_tree_tendermint no longer take hLInj as a hypothesis — only
FixedHash remains as a cryptographic assumption. This closes the load-bearing
residual assumption flagged in the threat-model analysis (the leaf-encoding
ambiguity class where Dragonberry-style F1/F2 bugs live).

SMT leaf injectivity (length := .noPrefix) needs the fixed-32-byte-hash argument
instead and is left as follow-up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
leafInj_noPrefix (LeafInj.lean): for a noPrefix leaf op over a fixed-length hash
(FixedHash H cs), two leaves hashing to the same value share their (key, value)
or exhibit a collision. The SMT shape is H_hash(prefix ++ H_phk k ++ H_phv v)
with no varint frame; both prehash digests are cs bytes wide, so the
concatenation splits at the fixed boundary via List.append_inj (the analogue of
varintEncode_append_inj). Instantiated as leafInj_smt (FixedHash H 32), axiom-free
beyond the fixed-width hash assumption.

All three shipped specs now have joint leaf injectivity proved from the encoding:
leafInj_tendermint / leafInj_iavl (varProto) and leafInj_smt (noPrefix + fixed
length). FixedHash moved from Tree.lean to Soundness.lean so LeafInj can use it
without an import cycle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extend Theorem B to every proof shape verify_non_existence accepts:

- nonexistence_sound_tree_leftOnly: a left-only proof's ensure_right_most
  path makes its neighbor the rightmost leaf (reaches_max), so
  maxKey t < key contradicts member_le_maxKey for any verifying
  existence proof.
- nonexistence_sound_tree_rightOnly: mirror via reaches_min /
  minKey_le_member.
- nonexistence_sound_tree_total: case split over the four neighbor
  shapes (verifyNonExistence_none shows no-neighbor proofs never
  verify), subsuming the two-sided theorem.
- nonexistence_sound_tree_tendermint_total: Tendermint instantiation,
  side conditions by decide, FixedHash the only assumption.

Extraction lemmas verifyNonExistence_{leftOnly,rightOnly,none} added to
NonExistSound.lean. Dropped the vestigial hkey hypothesis from the
two-sided tree theorems (membership_sound derives it internally).
Updated properties.md, which still described Theorem B as unproved.

Sorry count unchanged (1, the deliberate abstract-root statement).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The MTree model cannot represent sparse-merkle trees: SMT nodes may have
empty subtrees whose digest is the constant empty_child placeholder
(32 zero bytes for smt_spec, the same length as a real digest). Add
SMTree with an .empty constructor, rootHashS mapping it to the
placeholder, WFTreeS, and the one new explicitly-stated assumption
EmptyChildFree (no exhibited hash preimage of the placeholder, the
standard sparse-merkle assumption).

Theorem A transfers: reachesS reuses split_pins byte-level and rules
out folds terminating in an empty subtree via applyPath_ne_emptyChild
(a leaf-rooted spec-conformant fold is always a hash image, never the
placeholder). membership_sound_smt instantiates it for smt_spec with
leafInj_smt; remaining hypotheses are FixedHash and EmptyChildFree.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…total)

Non-existence soundness in the honest-root SMT tree model, covering all
verifier-accepted shapes (two-sided, left-only, right-only). The F4/F5
padding bridge that excluded the placeholder arm for Tendermint
(emptyChild.length != childSize) is replaced by bridges that embrace it:
an ensure_right_most step either has an empty suffix (genuine right
step) or a suffix exactly equal to emptyChild, which under
EmptyChildFree pins the skipped right sibling to a genuinely empty
subtree (rootHashS_ec_empty); mirrored for ensure_left_most via the
prefix's trailing 32 bytes. reaches_maxS / reaches_minS thus land on
the rightmost / leftmost *nonempty* leaf (Option-valued maxKeyS /
minKeyS), and neighbor_divergence_smt pins a two-sided proof to a
divergence node as before.

Key order throughout is on comparison keys (keyForComparison, i.e.
SHA-256-prehashed keys for smt_spec), with a members-based SortedTreeS
formulation that makes the BST gap argument compositional over empty
subtrees. nonexistence_sound_smt_total discharges every structural side
condition by computation; the remaining hypotheses are FixedHash,
EmptyChildFree, and sortedness of the honest tree on prehashed keys.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
properties.md: new "Remaining obligations" item 2a for the SMT model
and theorems; sections A and B now point at the proved theorems instead
of "proof in progress" / "not yet".

lean/README.md: the whitelisted sorry is the abstract-root
nonexistence_sound (not existence_binding, long since proved); complete
the file table; add the honest-root Theorem A/B and SMT entries to the
status list; update next steps.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Tendermint-shape membership_sound assumes min_prefix_length =
max_prefix_length, which IAVL violates (4-12 bytes of height/size/
version varints) and relies on digest length = child_size, which IAVL
also violates (33-byte amino child slots = 0x20 length byte + 32-byte
digest). Model an IAVL node as the existing MTree shape with a 1-byte
mid (the right child's 0x20 length byte) and a [min,max]-windowed
prefix (WFTreeI).

The pinning argument is replaced: split_pins_var shows the verifier's
suffix % child_size = 0 check together with max_prefix_length <
child_size pins the suffix length to 0 or child_size, and each choice
resolves the entire split to a genuine child via plain append
injectivity - no prefix-length pinning needed, so variable prefixes are
fine. The max < min + child_size well-formedness family is what makes
this work.

membership_sound_iavl discharges every structural side condition by
computation; with leafInj_iavl already proved, FixedHash is the only
remaining assumption.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tal)

Non-existence soundness in the honest-root IAVL tree model, covering
all verifier-accepted shapes (two-sided, left-only, right-only). With
this, honest-root Theorems A and B hold for all three shipped specs.

The order-theoretic machinery reuses TreeNonExist.lean verbatim:
maxKey/minKey/SortedTree, node_gap_no_member, the neighbor-walk
decomposition, and - because IAVL's emptyChild is [] (length 0 != 33)
- the Tendermint-style padding bridges instantiated at cs = 33. The
navigation lemmas (reaches_maxI, reaches_minI, neighbor_divergenceI)
are rebuilt on split_var_left/right/split_pins_var, which pin a node
split from the suffix length alone, accommodating IAVL's variable
4-12 byte prefixes and 33-byte child slots.

nonexistence_sound_iavl_total discharges every structural side
condition by computation; with leafInj_iavl already proved, FixedHash
is the only remaining assumption (key order is raw bytesLt since
prehash_key_before_comparison = false).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sha256.pad computed the zero-padding count with truncating Nat
subtraction: (56 - (len+1) % 64 + 64) % 64 clamps to (0 + 64) % 64 = 0
whenever (len+1) % 64 > 56, so the padded message was not a multiple of
64 bytes and the block loop silently dropped the trailing bytes. Every
message with length = 56..62 (mod 64) hashed incorrectly.

The three embedded validation vectors (0, 4, 6 bytes) and every
Tendermint/SMT test preimage happen to miss that window; the IAVL
differential vectors (57-byte leaf preimages) land exactly in it, which
is how the Phase 2a oracle caught this on its first run.

Replace with (120 - (len+1) % 64) % 64, which never truncates for
remainders in [0, 63], and add padding-boundary regression vectors
(55, 56, 57, 62, 63, 64 bytes) cross-checked against hashlib.

Scope: Sha256.lean is the executable layer only; the soundness theorems
are abstract over HashFn and were never affected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
rust/examples/lean_testdata.rs decodes every testdata/{iavl,tendermint,
smt} vector's protobuf CommitmentProof (18 vectors, using only existing
dev-dependencies) and generates lean/Ics23/TestVectors.lean: the
decoded proofs as Lean literals plus a native_decide acceptance check
per vector mirroring the Rust verify_membership/verify_non_membership
call. lake build now re-verifies every shared vector against the Lean
model with real SHA-256, so an accept/reject divergence between the
model and the implementations fails the proof build.

CI regenerates the file and fails if it drifts from the committed copy
(lean.yml now also triggers on testdata/ and the generator).

The oracle paid for itself immediately: on its first run it rejected
all six IAVL vectors, exposing the Sha256.pad truncating-subtraction
bug fixed in the previous commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
All eight findings from the July 2024 Zellic assessment recorded in
Corpus.lean under a new "Zellic audit findings" section, with
machine-checked model witnesses where the model has a decidable
counterpart:

- 3.1 (sorted-keys nonexistence soundness, Critical): reconstruct the
  Zellic Fig. 3.1 tree as an MTree; machine-check that "b" is a genuine
  member yet the tree is NOT SortedTree -- i.e. the exact SortedTree
  hypothesis Theorem B requires (modeling note F4).
- 3.2 (MaxPrefixLength >= MinPrefixLength + ChildSize, Critical): the
  boundary spec fails wellFormedB and the overlapping op fails
  ensureInner; this is the max < min + child_size bound the
  split-pinning lemmas rely on.
- 3.4 (zero child_size division, High): ensureInner rejects against a
  zero-child_size spec; model is %-total (no panic).
- 3.5 (unrestricted proof size, Medium): over-deep path fails
  checkExistenceSpec once the depth gate is armed; shipped specs
  disable it (min_depth = 0), faithfully reflecting the finding.
- 3.6 (out-of-bounds ChildOrder panic, Low): getPadding is total --
  getPadding {childOrder := [0,2]} 1 = none, no panic.

Implementation-only / out-of-scope findings recorded as notes: 3.3
(TypeScript, removed upstream), 3.7 (compressed batch proofs, out of
model scope), 3.8 (stricter IAVL/Tendermint checks -- baked into the
honest-root WFTree predicates).

properties.md: "Sources to mine" updated, a Zellic finding map table
added, and the pending obligations marked done.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wrap two multi-argument assert! calls across lines per rustfmt. No
logic change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant