Skip to content

feat(evm): runUntil, basic-block composition, and block tactics - #8

Merged
Th0rgal merged 1 commit into
mainfrom
feat/f4d-rununtil-block
Aug 25, 2026
Merged

feat(evm): runUntil, basic-block composition, and block tactics#8
Th0rgal merged 1 commit into
mainfrom
feat/f4d-rununtil-block

Conversation

@Th0rgal

@Th0rgal Th0rgal commented Aug 25, 2026

Copy link
Copy Markdown
Member

Summary

F4d in the EIP-8282 parent-strength campaign. Adds the basic-block layer on top
of the merged F4a step-run decomposition (EvmYul/EVM/Proof/Execution.lean).

EvmYul.EVM.Proof.Execution can peel X one instruction at a time, but nothing
could yet talk about a block — a maximal straight-line stretch of code, which
is the unit a correspondence proof actually wants. XRuns is not enough on its
own: any prefix of a run is a run, so it never says why the chain ended.

New file: EvmYul/EVM/Proof/Block.lean (one import line added to EvmYul.lean).

Halting is a property of the opcode

Halting decides the same thing H does, without the machine state.
H_eq_none_iff proves nothing was lost — H reads the machine state only to
fetch returned bytes on the branch that has already decided to stop. This is
what lets the executable driver decide to continue before running the
instruction.

Z is a gas frame

Z_ok_state: whenever X's exceptional-halting check accepts, the state it
hands to step is the pre-state with memory expansion charged and nothing else
touched. Without it every block lemma would have to be phrased against Z's
output, which is a state no caller can name. Z_ok_stack_length additionally
recovers the stack depth an accepted instruction is guaranteed to have.

Z's body is a long guard chain that simp cannot walk (it exhausts the step
budget) and split_ifs re-elaborates branch by branch, so elim_guard peels it
one guard at a time.

RunUntil and composition

RunUntil stop validJumps fuel pre trace rem post chains non-halting iterations
of X and ends only for a reason it records:

  • RunUntil.X_eqthe composition theorem: a block may be replaced by its endpoints.
  • RunUntil.stopped — the exit state decodes to a stop instruction, or the fuel is gone.
  • RunUntil.deterministic — a block is a partial function of entry state and fuel.
  • RunUntil.length / rem_le — fuel accounting, inherited from XRuns.
  • RunUntil.X_success / X_revert / X_success_of_halting / X_Z_error / X_step_error — a block plus its terminating instruction determines X's answer.
  • RunUntil.trans_step / X_eq_trans_step — chain block, boundary instruction, next block, which is what a control-flow path actually composes.

Stop conditions are just predicates on the opcode; CallFamily and BlockEnd
are supplied, and stopOrHalting widens any of them to also stop at halts,
since X stops there whether or not the caller asked.

Executable driver

xRunUntil walks the same block and is validated in both directions —
xRunUntil_sound and xRunUntil_complete — so it is a decision procedure for
RunUntil, and a concrete block can be discharged by evaluation while still
yielding a theorem about X.

Trace-erased runs and tactics

XReaches is XRuns with the trace existentially quantified — the form with no
index a tactic would have to invent. x_step peels one instruction from a
matching XStepAt hypothesis, x_block peels the whole block, and x_collapse
carries that through to an X goal. They search the local context and nothing
else, so they cannot invent a step that was never proved.

Non-vacuity

Every relation introduced here is inhabited beyond its reflexive/stopping cases,
proved symbolically for an arbitrary pre-state: xReaches_one_JUMPDEST,
runUntil_one_JUMPDEST. The tactics are exercised rather than merely defined:
xReaches_two_steps, xReaches_three_steps, X_eq_of_two_steps.

Notes for review

Test plan

  • lake build green on the whole library
  • #print axioms clean on all new results
  • Reviewer sanity-check of the Z_ok_state guard-peeling count against Z's body

`X` could be peeled one instruction at a time, but not reasoned about a
basic block at a time. `RunUntil` composes consecutive non-halting
iterations of `X` and records *why* the chain ended, so a correspondence
proof can be handed the fact that the continuation begins at a block
boundary rather than at an arbitrary prefix of a run.

Supporting this needed two facts about `X`'s internals: that halting is a
property of the opcode alone (`H_eq_none_iff`), which lets the driver
decide to continue before running the instruction, and that `Z` only ever
charges memory expansion (`Z_ok_state`), which lets block lemmas be
phrased against the state `X` decoded in instead of against `Z`'s
otherwise unnameable output.

`xRunUntil` is the executable driver, validated in both directions, so a
concrete block can be discharged by evaluation. `XReaches` is the
trace-erased form, and `x_step` / `x_block` / `x_collapse` discharge a
straight-line stretch from step facts in the context without the caller
naming any intermediate state.

Depends only on the merged F4a step-run layer. No axiom, no sorry.
@Th0rgal
Th0rgal marked this pull request as ready for review August 25, 2026 19:37
@Th0rgal
Th0rgal merged commit d164b61 into main Aug 25, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c688f4dbb5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

`JUMPDEST` is included because it is where control can *arrive*, so a block that
ran through one would not be maximal. -/
def BlockEnd (w : Operation .EVM) : Bool :=
Halting w || CallFamily w || w ∈ [Operation.JUMP, .JUMPI, .JUMPDEST]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Treat invalid opcodes as block ends

When execution reaches 0xfe or any unknown opcode, parseInstr decodes it as .INVALID and δ .INVALID = none, but this predicate returns false. Consequently xRunUntil BlockEnd attempts Z and returns .InvalidInstruction instead of producing an exit state, so the block-level RunUntil.X_Z_error theorem cannot be used for bytecode ending in an invalid opcode. Include .INVALID among the unconditional block-ending operations.

Useful? React with 👍 / 👎.

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