Skip to content

proof(ocapn): mechanize op:flush per-reference FIFO in Lean 4 + Veil#3249

Draft
kumavis wants to merge 6 commits into
claude/implement-ocapn-flush-iisn9from
claude/ocapn-flush-veil-proof
Draft

proof(ocapn): mechanize op:flush per-reference FIFO in Lean 4 + Veil#3249
kumavis wants to merge 6 commits into
claude/implement-ocapn-flush-iisn9from
claude/ocapn-flush-veil-proof

Conversation

@kumavis

@kumavis kumavis commented May 8, 2026

Copy link
Copy Markdown
Member

Summary

Stacks on top of #3248 with a mechanized proof, in Lean 4 + Veil, of the central per-reference FIFO claim that motivates the op:flush work in #3248.

packages/ocapn/proofs/veil/ is a self-contained Lake project that models a 3-party Alice / Bob / Carol abstraction of OCapN's flush-based promise-shortening protocol and discharges the cross-phase FIFO safety claim by SMT (Z3 / CVC5) via Veil's #check_invariants mechanism.

What is verified

For Bob's reference messages targeted at Alice's exported promise p1, partitioned into:

  • phase 1 — sent strictly before op:flush, traveling B→A.
  • phase 2 — sent after the handoff completes, traveling B→C→C→A.

The safety property is the cross-phase end-to-end FIFO claim:

phase1 m₁ ∧ phase2 m₂ ∧ processed m₂  →  processed m₁

i.e. once Alice has dispatched any phase-2 reference message to the entity, every phase-1 message has also been dispatched. This is the formal counterpart of the informal "Sandwich Lemma" in the proof reports already discussed on #3248.

How

OcapnFlush/Flush.lean declares a Veil transition system with:

  • mutable relations for phase classification, processed, and protocol-event flags;
  • 14 actions for each protocol step (Bob's op:flush send, Alice's swap, Bob's deposit-gift / handoff-give, Carol's withdraw-gift, etc.); each action's preconditions encode pairwise FIFO (A1) and the gift-availability gate (A3);
  • 8 inductive-invariant clauses + 1 safety clause — the two-fence chain:
    1. flushProcessedAtA → flushSentByB
    2. flushProcessedAtA → (phase1 M → processed M)flush fence
    3. depositGiftProcessedAtA → flushProcessedAtAB→A FIFO
    4. withdrawGiftProcessedAtA → depositGiftProcessedAtAgift availability
    5. cHoldsRef → withdrawGiftProcessedAtA
    6. phase2 M ∧ processed M → withdrawGiftProcessedAtAC→A FIFO
    7. processed M → (phase1 M ∨ phase2 M)
    8. ¬ (phase1 M ∧ phase2 M)

#check_invariants produces 144 SMT obligations (init + every clause × every action + safety) and reports for every one. A sat trace [happyPath] BMC obligation also confirms the protocol admits a 15-step non-trivial execution that ends with both a phase-1 and a phase-2 message processed.

The prove_inv_inductive and sat trace blocks are admitted with sorry (Veil's reconstructProofs := false), since the SMT verification — not Lean proof-term reconstruction — is Veil's verification of record. The README explains.

What is not in this model

  • Concurrent flushes / 4-party concurrent case (§4 of the alternate proof report).
  • Within-phase ordering (folded into Alice's processing-order preconditions).
  • Liveness — safety only.
  • Multi-sender per reference.

Build / install

The README documents the install process. On a vanilla machine:

cd packages/ocapn/proofs/veil
lake update                       # fetch Veil + transitive deps
lake build OcapnFlush.Flush       # downloads z3, cvc5, builds & checks

uv (used by Veil for SMT-model wrappers) needs to be reachable; if astral.sh is blocked you can install it via pip install uv and copy the binary into .lake/packages/veil/.lake/build/uv. Output ends with:

warning: OcapnFlush/Flush.lean:248:0: Trusting the SMT solver for 144 theorems.
warning: OcapnFlush/Flush.lean:265:0: declaration uses 'sorry'   # prove_inv_inductive (admitted)
warning: OcapnFlush/Flush.lean:275:0: declaration uses 'sorry'   # sat trace bmc (admitted)
Build completed successfully (723 jobs).

Test plan

  • lake build OcapnFlush.Flush succeeds; all 144 SMT clauses reported .
  • sat trace [happyPath] BMC trace produces a satisfying 15-step execution.
  • No mathematical sorry beyond the two SMT-discharged ones (documented).

https://claude.ai/code/session_013hZeUaQrKVRoZQjpd1vcs4


Generated by Claude Code

@changeset-bot

changeset-bot Bot commented May 8, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 7d229a7

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

claude added 2 commits May 8, 2026 23:56
Implement the receiver-initiated flush handshake that preserves per-reference
FIFO order when a peer is about to shorten a promise (typically as part of a
third-party handoff).

When the receiver sends op:flush for an exported promise position, the
exporter mints a fresh local promise, swaps it in at the same export position
(keeping the slot's refcount), and replies with op:flush-done. Further
deliveries the receiver had already sent for that position arrive before the
ack and dispatch on the original promise; deliveries sent after ack are
serialized through the new promise and naturally buffer until the shortened
reference resolves it.

Also exposes _debug.flushExport on Ocapn for tests, and adds replaceExportValue
on the pairwise table to support in-place value replacement at a slot.
Move op:flush / op:flush-done into the existing OcapnMessageUnionCodec
snapshot table so the wire form is captured by the same convention as the
other operations, and update the snapshots. Drop the now-redundant manual
round-trip tests.

The flush behavior tests previously had B return an unresolved promise
directly, which blocks the deliver-fulfill until the promise settles and
prevented the export from being registered. Wrap the promise in an array so
the array (a copyArray) is the immediate fulfillment value and the embedded
promise is serialized as desc:import-promise on the wire.

Also satisfy lint: thread the void/Promise<void> conversion of quietReject
through unknown, and silence no-use-before-define on captures of send,
didUnplug, and quietReject inside the new handler / flushExport.
@kumavis kumavis force-pushed the claude/implement-ocapn-flush-iisn9 branch from f8ea0e2 to 40aee83 Compare May 8, 2026 23:57
claude added 2 commits May 9, 2026 01:16
Add packages/ocapn/proofs/veil — a Lake project that mechanizes the central
safety claim of the op:flush proposal in the Veil framework on top of Lean
4.24.

The model is a 3-party Alice/Bob/Carol abstraction:

- Bob is the sole sender of reference messages targeting Alice's exported
  promise p1, partitioned into two phases: pre-flush messages (B->A) and
  post-handoff messages (B->C->C->A after the handoff completes).
- The flush handshake (op:flush, op:flush-done), the deposit-gift / handoff-
  give / withdraw-gift sequence, and Alice's pairwise-FIFO processing order
  are encoded as 14 Veil actions whose preconditions capture A1 (pairwise
  FIFO) and A3 (gift availability) of the informal proof.

The verified safety property:

    phase1 m1 ∧ phase2 m2 ∧ processed m2 → processed m1

i.e. once Alice has dispatched any phase-2 reference message, every phase-1
reference message has likewise been dispatched - the cross-phase end-to-end
FIFO claim. The two-fence chain from the informal proof is captured by 8
inductive-invariant clauses; #check_invariants emits 144 SMT obligations and
discharges them all (Z3/CVC5).

A 15-step BMC trace witnesses that the protocol admits a non-trivial happy-
path execution.

See packages/ocapn/proofs/veil/README.md for build instructions and a
mapping back to the informal proof's "Sandwich Lemma".
Add .github/workflows/ocapn-flush-proof.yml. The job runs on PRs (and on
master pushes) that touch packages/ocapn/proofs/, sets up Lean 4 via
leanprover/lean-action, fetches Veil + transitive deps with `lake update`,
and runs `lake build OcapnFlush.Flush` while capturing the build log.

After the build completes the workflow asserts the verification shape:

- no clause in the inductive-invariant table reports ❌,
- at least one ✅ marker is present (guards against the
  `#check_invariants` step silently disappearing),
- Veil's "Trusting the SMT solver for N theorems" line is emitted, and
- exactly two `sorry`-admitted declarations remain in the proof file
  (`prove_inv_inductive` and `sat trace [happyPath]`, both documented in
  the README and corresponding to claims the SMT solver has already
  validated).

Veil's downloadDependencies step also pulls `uv` from astral.sh; the job
pre-installs `uv` via pip and stages it into the expected build path so
the toolchain bootstrap is robust against astral outages. The build log
is uploaded as an artifact for postmortem.
@kumavis kumavis force-pushed the claude/ocapn-flush-veil-proof branch from 49bbf40 to c6d5619 Compare May 9, 2026 01:16
claude added 2 commits May 9, 2026 01:50
The repo's GitHub Actions runtime policy forbids any non-pinned action,
including transitively-referenced ones inside composite actions. The
leanprover/lean-action@v1.5.0 composite calls
`actions/cache/restore@v5` (no SHA), so the workflow failed to start
even though lean-action's own SHA was pinned.

Replace the lean-action step with a self-contained installer that
parses `lean-toolchain`, downloads the matching Lean release tarball
from GitHub (which is allowed), extracts it, and adds the bin dir to
PATH. Reorder the uv pre-stage to run after `lake update`, since the
.lake/packages/veil tree only exists after dependency resolution.
…trivial interleavings

Add OcapnFlush/FourParty.lean: a Veil model of two sequential flush +
handoff cycles in a chain Alice / Bob / Carol / Dave. Bob hands the
reference to Carol via the existing flush handshake, then Carol in turn
hands it to Dave via a second flush handshake. Reference messages are
partitioned into three phases (phase1 = Bob's pre-flush messages, phase2
= Carol's between-handoffs messages, phase3 = Dave's post-handoff
messages).

The key new fact the model exercises is that phase-2 traffic may flow
freely while Carol is preparing and running her own flush — the action
preconditions deliberately do *not* require phase-2 to be drained
before Carol initiates her flush. The fence chain doubles up: one
B↔A chain transitions phase1→phase2, one C↔A chain transitions
phase2→phase3. The linking invariant `cdFlushSentByC → cHoldsRef`
is what forces Carol's flush to be causally downstream of Bob's
handoff, letting the two chains compose.

Verification:

- `#check_invariants` emits 580 SMT obligations (init + actions × 21
  invariant clauses + 3 safety clauses), all `✅`.
- `sat trace [twoShortenings]` runs a 30-step trace where Carol's
  flush initiation is interleaved between the first phase-2 send and
  its Alice-side processing.

Same `sorry` footprint as Flush.lean: `prove_inv_inductive` and the
`sat trace` BMC are admitted to the SMT solver via Veil's
`reconstructProofs := false`.

Update the CI workflow to build the entire library (not just
Flush.lean) and to count admitted-declaration sorrys per module
(currently 2 per file). Update the README accordingly, including a
discussion of which interleavings the new module covers and which
(fully-symmetric concurrent flushes at adjacent intermediaries) it does
not.
@kumavis kumavis force-pushed the claude/implement-ocapn-flush-iisn9 branch 3 times, most recently from ee674ad to 19c0bbf Compare May 11, 2026 08:15
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.

2 participants