Skip to content

verity: C13 accept-path climb-correspondence workbench#3

Open
Th0rgal wants to merge 21 commits into
mainfrom
verity-sphincs-minus-model
Open

verity: C13 accept-path climb-correspondence workbench#3
Th0rgal wants to merge 21 commits into
mainfrom
verity-sphincs-minus-model

Conversation

@Th0rgal
Copy link
Copy Markdown
Member

@Th0rgal Th0rgal commented Jun 1, 2026

Summary

Builds out the C13 MODEL-EXEC-BRIDGE discharge — the segment-by-segment
proof that the compiled Verity model's keccak accept path refines the byte
spec. (Follows up the merged #1; depends on the keccak256/calldata
interpreter support in lfglabs-dev/verity#1943.)

  • Spec mirror/concrete kernels (C13Concrete, C13Mirror + axiom files).
  • Per-segment interpreter characterisations: SegmentSeed, SegmentS2,
    SegmentS2R (H_msg, proved unconditional), SegmentS3,
    SegmentS4Fors, SegmentS4Finalize, SegmentLayer3, SegmentCompose,
    SegmentAcceptSpec.
  • Reusable climb engines: ClimbKit, ClimbLoop, ClimbLoopGuarded,
    ClimbKeccakStep, ClimbMemFrame, ClimbMemFrameMerkle, ClimbStepSpec,
    KeccakBridge, MemoryKit, MkC13State, RootFrame, BindingFrame,
    PreflightC13.

The Merkle-climb per-step correspondence (stepMerkle_eq_merkleSpecStep,
xmssClimb_eq_specFold) and ADRS-word assembly (address_assembly_eq) are
axiom-clean ([propext, Classical.choice, Quot.sound]).

Still open (the 3 named bridge axioms remain): the per-iteration
auth-path calldata correspondence (blocker #20), the Layer-3 hypertree
compose, and the atomic execC13 flip. No sorry anywhere.

Test plan

  • lake build SphincsMinusVerifiers green (against verity#1943 branch)
  • #print axioms on per-verifier theorems shows only [propext, <prims>, <bridge axiom>]

Note

Medium Risk
Large formal-verification surface on the SPHINCS C13 accept path; mistakes could block or mislead the eventual bridge proof, but the change is additive and leaves the live bridge axiom and opaque executor untouched.

Overview
Adds a large additive Lean layer for discharging the C13 MODEL-EXEC-BRIDGE: concrete byte-spec primitives, a segment-aligned spec mirror, reusable climb/keccak/frame engines, and synced audit/axiom docs. execC13 and c13_refines_byte_spec are unchanged (still opaque/axiom).

C13Concrete introduces c13PrimitivesConcrete with hashing via the same KeccakEngine / big-endian word preimages as the Verity interpreter (keccakWords, maskN, tweakable hash). It implements C13 parsing, H_msg, FORS/WOTS/XMSS reconstruction, and many spec-side lemmas (16-byte canonical outputs, auth-path offsets, hypertree root shape) without new axioms or sorry.

C13Mirror factors verifyBytes into named intermediates (S2 digest, S3 forced-zero, S4 FORS, Layer-3 climb) and proves verifyBytes_c13_eq_mirror so later segment proofs can compose against one RHS.

Climb stack (ClimbKit, ClimbLoop, ClimbLoopGuarded, ClimbKeccakStep, ClimbMemFrame, BindingFrame) proves interpreter step/loop shape for branchless Merkle and WOTS bodies, guarded hypertree loops, bounded evalExpr for masked keccak and ADRS assembly, and memory/binding frame preservation for keccak correspondences.

AUDIT.md / AXIOMS.md record partial progress: concrete c13Primitives, build green, standalone lemma footprints; bridge theorem, auth calldata (#20), and full compose still open.

Reviewed by Cursor Bugbot for commit dd1e753. Bugbot is set up for automated code reviews on this repo. Configure here.

Adds the layered C13 MODEL-EXEC-BRIDGE discharge workbench: the spec
mirror/concrete kernels (C13Concrete/C13Mirror), the per-segment
interpreter characterisations (SegmentSeed/S2/S2R/S3/S4Fors/S4Finalize/
Layer3/Compose/AcceptSpec), and the reusable climb engines (ClimbKit,
ClimbLoop, ClimbKeccakStep, ClimbMemFrame[Merkle], KeccakBridge,
MemoryKit, MkC13State, RootFrame, BindingFrame, PreflightC13).

Segment S2 (H_msg) is proved unconditional; the Merkle-climb per-step
correspondence (stepMerkle_eq_merkleSpecStep, xmssClimb_eq_specFold) and
ADRS-word assembly (address_assembly_eq) are axiom-clean. The residual
open obligation is the per-iteration auth-path calldata correspondence
(blocker #20) and the Layer-3 hypertree compose that flips execC13 from
opaque to a concrete interpreter run. The 3 bridge axioms still stand.

Full `lake build SphincsMinusVerifiers` green, no sorry.
@Th0rgal
Copy link
Copy Markdown
Member Author

Th0rgal commented Jun 1, 2026

@codex review

@chatgpt-codex-connector
Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

Th0rgal added 20 commits June 1, 2026 10:17
Add SiblingCalldata.lean with masked_sig_read_eq_wordOfHash16: for any
16-byte-aligned signature offset, the masked calldata word equals the
spec's wordOfHash16(read16 sig sOff). Covers R, FORS sk, and FORS auth
reads. Split into aligned/straddle case lemmas; zeta-reduce the
calldataloadWord let-bindings via an explicit defeq `show` to avoid a
kernel deep-recursion from the reduceIte/reduceMul simprocs. Axiom-clean.
Add `read32BE_calldata` (frozen calldata word = 32-byte big-endian read for
ANY offset, via low/high stitching across the 32-byte word boundary in the
straddle case) and `masked_sig_read_eq_wordOfHash16_gen`, dropping the
`sOff % 16 = 0` hypothesis. Covers the XMSS auth offsets (≡4/≡8 mod 16).
Built on new readBE split bricks (readBE_split_at/_div, word_eq_readBE,
readBE_eq_read16). Axiom-clean (propext, Classical.choice, Quot.sound).
Add `merkleClimbData_of_frozenCalldata`: discharges the per-height
`MerkleClimbData auth cdAt h` (fully-reduced blocker #20, definitionally
`maskN (cdAt h) = wordOfHash16 auth[h]`) against the frozen mkC13State
calldata, given that `cdAt h` is the frozen word at `sigDataOffset + sOff`
and the spec parse fact `auth[h] = read16 sig sOff`. Closes via
`SiblingCalldata.masked_sig_read_eq_wordOfHash16_gen` (maskN is defeq
`Nat.land · N_MASK`). Works for ANY offset, incl. XMSS auth ≡4/≡8 mod 16.
Imports SiblingCalldata (no cycle). Axiom-clean; full build green.
Pure-spec list-indexing lemmas pinning every climb auth-path sibling to
its sig byte-offset:
  * XMSS: layer.authPath[h]? = read16 sig (1952+868*layer+692+16*h), h<11
  * FORS: fors.authPath[t][h]? = read16 sig (128+304*t+16*h), t<6,h<19
plus getElem?_map_range helper and parseSignatureC13_size. Axiom-clean
(propext only).
climb_calldata_read_eq_frozen (P2): the per-step `calldataload(authPtr +
h<<4)` in the model resolves to the frozen-image word at `sigDataOffset +
sOff`, via evalExpr_siblingOffset + offset arithmetic (no execC13).

xmss_climb_data_range (P3): composes parseSignatureC13_layer_authPath_getElem?
(hauth) with merkleClimbData_of_frozenCalldata to supply the whole-loop
`∀ i<11, MerkleClimbData lsig.authPath cdAt i` range hypothesis consumed by
foldLoop_invariant_cond. Both axiom-clean (propext, Classical.choice, Quot.sound).
FORS analog of xmss_climb_data_range: the FORS outer loop's inner Merkle
climb (forsLeafBody, forEach h<19 over the same merkleClimbBody) gets its
whole-inner-loop range hypothesis ∀ h<19, MerkleClimbData tAuth cdAt h from
parseSignatureC13_fors_authPath_getElem? (hauth) composed with
merkleClimbData_of_frozenCalldata, with authPtr = sigDataOffset + (128+304·t).
Axiom-clean (propext, Classical.choice, Quot.sound).
merkleClimb_foldLoop_correspondence: foldLoop_invariant_cond specialised to
the Merkle climb (R=MerkleClimbRel, specStep=merkleSpecStep, D=MerkleClimbData,
step=stepMerkle). The whole climb loop advances MerkleClimbRel together with
specFold, gated on the per-step advance hstep and the range hypothesis supplied
by xmss/fors_climb_data_range.

xmssClimb_model_node: combines the above (node projection) with
xmssClimb_eq_specFold to give the "model node = spec xmssClimb root piece"
equality, conditional on hstep (frame bookkeeping at the segment-trace
climb-entry state) and hR (initial node = WOTS-PK/FORS-leaf keccak).

Both axiom-clean (propext, Classical.choice, Quot.sound).
…l projection

MerkleClimbRel (idx+node bindings only) cannot self-preserve under one stepMerkle
step. MerkleClimbFrame bundles it with the static frame the step lemma needs
(adrsBaseVar/authPtrVar bindings, mem[0x00]=seed, selector=0, frozen calldata
image, name-distinctness). MerkleClimbFrame.toRel projects back to MerkleClimbRel.
Axiom-clean ([propext, Quot.sound]).
Add three frame-preservation lemmas proving one merkleClimb step leaves the
non-rebound frame components fixed:
- stepMerkle_selector_calldata: selector and world.calldata unchanged
- stepMerkle_binding_frozen: any binding w distinct from the five rebinds
  (sibling/parentIdx/s/nodeVar/idxVar) is preserved
- stepMerkle_mem_zero: seed cell mem[0x00] preserved (writes land at 0x20/o5/o6)

All axiom-clean [propext, Classical.choice, Quot.sound].
Resolves the climb body's parity-xored child slots (merkleClimbBody stmts 5/6,
0x40 ^^^ s and 0x60 ^^^ s) to bare Nat.xor on bounded operands, mirroring
evalExpr_bitOr_bounded. Axiom-clean.
Add three generic combinators resolving the h2/h4/h5off/h6off evalExpr
hypotheses of MerkleClimbRel_step from variable bindings alone:
- eval_parentIdx_shr: shr(1, idxVar) = mIdx >>> 1
- eval_selector_shl: shl(5, and(idxVar,1)) = (mIdx &&& 1) <<< 5
- eval_childOffset_xor: xor(off, s) = Nat.xor off sval

h5val/h6val are bare .localVar reads (definitional at the assembly site); h1 is
merkle_sibling_read_frozen, h3 is address_assembly_eq. All axiom-clean.
Assemble MerkleClimbFrame_step: one climb step carries the whole frame
(relation + adrsBaseVar/authPtrVar bindings + seed cell + selector + calldata
image + name-distinctness) forward, given the same per-step bundle
MerkleClimbRel_step consumes. Relation advance via MerkleClimbRel_step; static
conjuncts via the three frame-frozen projections; distinctness tail carried
verbatim. Does not discharge h1..h6val/StepDataObligations (offset/address/
sibling content, isolated in the per-fact combinators + MerkleClimbData).
Axiom-clean.
MerkleClimbFrame_h_inject (frame survives the loop's "h" rebind),
merkleClimbFrame_foldLoop_correspondence and xmssClimbFrame_model_node
(foldLoop_invariant_cond specialised to the frame invariant), so the
static frame rides the loop invariant and each per-step hstep instance
receives it for free. Axiom-clean.
Composes MerkleClimbFrame_h_inject + MerkleClimbFrame_step to produce the
exact hstep body the frame-carrying loop lift demands, folding the static
frame in so a concrete climb-entry instantiation supplies only the per-step
data facts (h1 bottoming at the site-specific offset identity). Axiom-clean.
Add SegmentS4ForsDataObligations with two axiom-clean reductions for the
remaining FORS data obligations of
C13SeedNamedAcceptGuardedPkRootSizeLeafObligations:

* hLeaf_of_stepMerkle_seed_frame reduces the hLeaf field (one FORS
  leaf-step preserves the public-seed cell mem[0x00] for idx < 6) to a
  single unconditional per-step stepMerkle seed-cell frame for the inner
  FORS Merkle climb.

* hmRlo_of_afterFors_root_slots reduces the hmRlo field (the six ordinary
  FORS root cells 0x80 + 32*j, j < 6, after forsFinalizePreCopyStep) to the
  afterFors root-slot correspondence; the pre-copy finalize prefix is a
  frame over those six source slots.

Both depend only on [propext, Classical.choice, Quot.sound]. Standalone:
they touch neither execC13 nor c13_refines_byte_spec, and introduce no
axiom, sorry, or native_decide.
…rame

Removes the residual hstep hypothesis of hLeaf_of_stepMerkle_seed_frame:
stepMerkle_seed_frame_unconditional proves one branchless Merkle swap step
preserves mem[0x00] for every state (no pathIdx < 2^256 bound, via the parity
witness n := pathIdx % 2^256 and the new bound-free evalExpr_bitAnd_literal_modself).
hLeaf_discharged then closes hLeaf outright. #print axioms: foundational only.
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit dd1e753. Configure here.

as written by `mstore(0x80, 0xFF…FB...)` — the contract literal has a leading
zero nibble (63 `F`'s). -/
def hMsgPad : Word :=
0x00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hMsgPad has 64 F-nibbles instead of 63

High Severity

hMsgPad is defined as 0x00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF (leading 00 + 64 F's). Since Word := Nat, the leading 00 is a no-op, making this 2^256 - 1. However, the contract model in Model.lean and SegmentS2.lean uses a literal with 63 F's (2^252 - 1), and the comment on hMsgPad itself says "the contract literal has a leading zero nibble (63 F's)." The spec constant doesn't match the contract model, so hMsgC13 computes the wrong H_msg digest.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit dd1e753. Configure here.

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