Machine-check why gas() cannot enter the verified fragment yet - #148
Machine-check why gas() cannot enter the verified fragment yet#148leonardoalt wants to merge 1 commit into
gas() cannot enter the verified fragment yet#148Conversation
`opTable` maps `Op.gas` to `none`, so every program reading `gas()` is
rejected — which is every real solc contract, since `call(gas(), …)` is the
idiomatic call. This records, as proofs rather than prose, exactly what stands
in the way, so the next attempt starts from a known position instead of
rediscovering it.
Two independent obstacles.
**1. A side condition on the callee cannot be weakened to a threshold.**
`GasInsensitive` ("the callee behaves the same however much gas it is
forwarded") is false of real contracts: too little gas and any callee fails.
The natural repair is `GasThreshold` — for each request there is a `G` above
which behavior stops changing — which *is* true of real contracts, and whose
`G` depends only on request and state, never on the gas field, so a phase-B
bound may be chosen from it (`GasInsensitive.gasThreshold` shows flat
insensitivity is its `G = 0` case).
It still does not suffice, because the pinned oracle may report *any* word.
`thresholdCalls` is a callee that succeeds above 1000 gas and fails below;
`thresholdCalls_source_asteps` is a genuine two-step `op gas ; op call`
derivation in which the oracle reports `0` and the call therefore yields `0`;
and `thresholdCalls_no_admissible_allowance` proves no at-or-above-threshold
allowance reproduces it. A fused rule quantifying its allowance as "some
`g ≥ G`" would have no matching step, so its simulation would be *false*, not
merely unproved. With an unconstrained oracle, flat insensitivity is not just
sufficient but necessary — which is why the fix must remove that reading at
source (powdr-labs/yul-semantics#41) rather than constrain our own instruction.
**2. The phase-B gas bookkeeping cannot express a call's cost.**
`astep_sim` carries `∃ bnd, ∀ s, … ∧ s.gasAvailable - bnd ≤ s'.gasAvailable`.
Under EIP-150 a call forwards 63/64 of what remains, so the loss scales with
`s.gasAvailable`, unknown when `bnd` is fixed; `no_additive_bound_under_eip150`
proves no `bnd` covers it. This is the shape of our own statement, not any
hypothesis about environments, so no side condition repairs it. The fix is to
generalise from "subtract `bnd`" to a monotone gas transformer (`g ↦ (g - c)/64`
at a call, `g ↦ g - c` elsewhere) — closed under composition, discharged by
EIP-150 itself, and needing no assumption. `arun_halt_sim`'s conclusion has no
gas conjunct, so the headline statements would not change.
Nothing here is reachable from a headline theorem: the audited surface,
`SPEC.md`, `Checks.lean`, the pinned semantics and every baseline are
untouched, and `gas()` stays rejected exactly as before.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CI summary — ✅ All goodhead 1. Parsing
2. CorrectnessCompilation (positive corpora):
Behaviour differential vs solc:
3. Gasa) This compiler vs solc's optimized output — we compile solc's unoptimized
ours/solc > 100% is expected: this compiler has no Yul optimizer yet, so it spends more gas than solc's optimized output. This number is the size of that gap. It does not fail CI; only a regression above the pinned baseline does. b) Backend codegen parity — both this compiler and solc assemble the same, unoptimized Yul (solc
Here ours/solc near 100% is expected — neither side optimizes, so this compares raw code generation on identical input, not optimizer quality. 4. Compiler runtime (informational)Both columns measure the same job on the same input: unoptimized Yul → EVM bytecode, no optimizer on either side, over the same fixtures — only those both compilers finished are counted, on either side. solc's Solidity→Yul front-end is charged to neither: it runs once, before both, and its output is what each then compiles. a) Solidity corpora — both compile the unoptimized
Charged to neither column: 33.2 s of solc b) Yul corpora — the fixtures are already Yul, so both compile it directly; there is no front-end on either side.
Excluded from both columns: 1.3 min this compiler spent on 303 fixture(s) it then rejected. solc is not asked for those. Each figure is the sum of that suite's per-fixture compile spans, added across shards — independent of worker count and sharding, but measured on shared CI runners under saturated parallelism. Treat single-digit percentage moves as noise. Nothing here affects the verdict. 5. Soundness (formal guarantee)
6. Verdict✅ All good |
Standalone, additive, and deliberately does not add
gas()support.opTablemapsOp.gastonone, so every program readinggas()is rejected — which is every real solc contract, sincecall(gas(), …)is the idiomatic call. This records, as proofs rather than prose, exactly what blocks it, so the next attempt starts from a known position.I did implement the feature first (fused
GAS ; CALLAsm instruction, peephole rewrite, both simulation phases). It worked — the aaveLiquidationLogicfixture compiled with it and was rejected without it. It is not in this PR, because the side condition it needed turned out to be false.Obstacle 1 — the callee side condition cannot be weakened to something true
The working version assumed
GasInsensitive: the callee behaves the same however much gas it is forwarded. That is false of real contracts — too little gas and any callee fails.The natural repair is
GasThreshold: for each request there is aGabove which behavior stops changing. This is true of real contracts, and it fixes the part I expected to be fatal —Gdepends only on request and state, never on the gas field, so a phase-B bound can be chosen from it.GasInsensitive.gasThresholdshows flat insensitivity is just itsG = 0case.It still does not suffice, because the pinned oracle may report any word:
thresholdCalls— a callee that succeeds above 1000 gas and fails below;thresholdCalls_source_asteps— a genuine two-stepop gas ; op callderivation where the oracle reports0, so the call yields0;thresholdCalls_no_admissible_allowance— no at-or-above-threshold allowance reproduces it.So a fused rule quantifying its allowance as "some
g ≥ G" has no matching step: its simulation would be false, not merely unproved. With an unconstrained oracle, flat insensitivity is not merely sufficient but necessary — which is why the fix must remove that reading at source (powdr-labs/yul-semantics#41) rather than constrain our own instruction.Obstacle 2 — the phase-B gas bookkeeping cannot express a call's cost
Independent of any hypothesis.
astep_simcarries∃ bnd, ∀ s, … ∧ s.gasAvailable - bnd ≤ s'.gasAvailable. Under EIP-150 a call forwards 63/64 of what remains, so the loss scales withs.gasAvailable— unknown whenbndis fixed.no_additive_bound_under_eip150proves nobndcovers it.This is the shape of our own statement, not an environment assumption, so no side condition repairs it. The fix is to generalise from "subtract
bnd" to a monotone gas transformer (g ↦ (g - c)/64at a call,g ↦ g - celsewhere): closed under composition, discharged by EIP-150 itself, needing no assumption.arun_halt_sim's conclusion has no gas conjunct, so headline statements would not change.Prerequisites this establishes for a future attempt
ExternalGas), thenGasThresholdremains, and that one is true of real contracts.Scope
Additive: one new file plus its import. Nothing here is reachable from a headline theorem — audited surface,
SPEC.md,Checks.lean, pinned semantics and every baseline are untouched, andgas()stays rejected exactly as before.lake buildclean,Checks.leanclean,SpecClosure.leanclean andSPEC.mdin sync, nosorry/axiom/unsafe.🤖 Generated with Claude Code