Skip to content

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

Draft
Th0rgal wants to merge 81 commits into
mainfrom
verity-sphincs-minus-model
Draft

verity: C13 accept-path climb-correspondence workbench#3
Th0rgal wants to merge 81 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

High Risk
Touches formal verification of a cryptographic signature verifier and changes byte-spec final-root comparison and hypertree semantics; bridge axioms remain, but mistakes in spec or proof glue would undermine correctness claims.

Overview
Adds concrete byte-spec primitive packages for SPHINCS-C13 and C12: c13PrimitivesConcrete / c12PrimitivesConcrete route hashing through the same interpreter-aligned keccakWords kernel as SourceSemantics, with variant-specific parse layouts, H_msg, FORS/WOTS/XMSS reconstruction, and many parser/shape/canonical lemmas for later bridge proofs.

Spec layer updates extend Primitives with layer-indexed WOTS/XMSS hooks, switch the hypertree fold to wotsGrindingFailsAtLayer / *AtLayer reconstruction, and replace raw root == pk.pkRoot with rootMatchesPk (including lowBytesProjection when fullPkRoot).

Mirror and glue: C13Mirror names S2–S4 / layer-climb intermediates and proves verifyBytes_c13_eq_mirror; C13BridgePrep packages observed execC13 outcomes against verifyBytes on accept/reject subdomains; BindingFrame and ClimbKeccakStep add keccak-free binding frames and evalExpr identities for masked keccak and ADRS words.

Docs: AUDIT.md / AXIOMS.md record partial MODEL-EXEC-BRIDGE progress (c13_refines_byte_spec and opaque execC13 still open).

Reviewed by Cursor Bugbot for commit af8f42e. 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.

@Th0rgal Th0rgal marked this pull request as draft June 2, 2026 16:11
@Th0rgal
Copy link
Copy Markdown
Member Author

Th0rgal commented Jun 2, 2026

Checkpoint pushed at af8f42e.

Status: this is still WIP, not the final MODEL-EXEC-BRIDGE discharge. c13_refines_byte_spec and c12_refines_byte_spec remain named bridge axioms; the PR is draft for that reason.

Local verification before push:

  • lake build SphincsMinusVerifiers passed
  • git diff --check passed
  • Lean scan found no code sorry, admit, sorryAx, or exec := verifyBytes; matches were only in documentation/comments

Current progress in this checkpoint:

  • C13 byte-spec final comparison now uses rootMatchesPk c13, matching the projected model comparison.
  • Added concrete runner/support modules and C13 bridge-prep reducers.
  • Added C13 FORS/root/current-node/reject-path support lemmas.
  • Added C12 concrete primitive scaffold, but C12 remains behind C13.

Remaining before ready-for-review:

  • Universal C13 layer success data: executable digitSum = 208 and post-step currentNode = spec root for both layers.
  • Universal C13 layer revert data: executable digitSum after checksum equals C13Concrete.wotsDigitSum for layer 0 and layer 1.
  • Atomic replacement of the C13 bridge axiom with theorem once the above is closed, then port the pattern to C12.

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