Stop false 'keccak/unstructured storage is unreachable' skips; gate skip justifications#39
Stop false 'keccak/unstructured storage is unreachable' skips; gate skip justifications#39shellygr wants to merge 7 commits into
Conversation
Generated specs skipped properties whose state lives in keccak-derived storage slots, citing CVL inexpressibility. Keccak slots are compile-time constants reachable via Sload/Sstore hooks, ghosts, or harness getters; five coordinated changes stop both the author and the judge from blessing such skips: - prop_inference.py: narrow backend-guidance item 2 to hash collisions/inversion; keccak-derived slot constants and unstructured storage are explicitly in scope. - property_generation_prompt.j2: new <storage_access> block (slot-constant definitions, Sload/Sstore hooks incl. KEY patterns, ghost mirroring, harness getters, KB search pointer). - property_judge_system_prompt.j2: "No Source Changes" becomes "No Protocol Source Changes" -- harnesses are verification infrastructure. The demand-a-harness wording is gated on harness_augmentation (default(false), a no-op until harness augmentation lands); the fallback labels such skips "missing harness support, not CVL inexpressibility". - property_judge_prompt.j2 Criteria 7: capability checklist (slot constant + hook, harness getter/direct storage, ghost + summary) the judge must run before accepting a "not expressible" skip. - cvl_generation.py: _RecordSkipSchema requires alternatives_considered; access-shaped skip reasons (keccak/storage slot/unstructured/no getter/cannot access|observe) are rejected until each triggered capability is addressed. Adds render smoke tests for the modified templates (with/without harness_augmentation) and skip-gate acceptance/rejection tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…, collision carve-out Three review findings on the skip-gate change: - property_judge_prompt.j2: the Criteria 7 immutability carve-out still blessed any skip fixable "with Solidity code changes", contradicting the checklist item (b) six lines above and the protocol-vs-harness system prompt. It is now scoped to *protocol* code only, and explicitly states that needing a harness getter/wrapper does not validate a skip. - SkippedProperty carries alternatives_considered (defaulted for cached pre-field instances) and the property_feedback_judge input renders each alternative under the skip line, so the judge audits their substance instead of the gate keyword-matching them and dropping the text. The field stays out of _compute_digest (skip identity remains title+reason). - The bare "keccak" gate trigger fired on collision/inversion/preimage skips, which the guidance keeps legitimate; the trigger now requires keccak near "slot"/"storage" (the access-shaped usage the gate targets). Adds tests: collision reason passes the gate, alternatives land on the recorded skip, back-compat deserialization without the new field, and a render pin that the old blanket immutability wording is gone. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review finding: the Criteria 7 directives ("if any of these mechanisms
applies, reject the skip"; "needing a harness getter or wrapper does NOT
make a skip valid") told the judge to reject harness-requiring skips even
in pipelines where the author has no tool to create harnesses, tensioning
with the system prompt's harness_augmentation=false fallback and burning
author/judge rounds until the rebuttal escape hatch fired.
Criteria 7 now branches on the same harness_augmentation variable as the
system prompt (default false): the fallback keeps rejection for the
always-available mechanisms (slot constant + hook, direct storage access,
ghost mirroring) but accepts harness-getter-only skips with the "missing
harness support, not CVL inexpressibility" label, matching the system
prompt wording. FeedbackInherentParams gains the NotRequired param so
PR 2 can bind both templates consistently; current call sites omit it.
Render tests pin both branches and omitted==explicit-false equivalence.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…shaped skip triggers
The judge audits alternatives_considered (feedback.py surfaces them), so
mutating them after validation must stale the PROVER stamp; empty lists
contribute nothing, keeping cached pre-field digests valid. Also widen the
skip-gate triggers to `cannot (be )?(access|observ)` so passive phrasings
("cannot be accessed/observed") no longer bypass the gate. Tests pin both.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
In greenfield the author defines the contract API through the stub, so a missing getter is never a capability gap: getter-shaped skips stay rejectable there instead of being accepted with the fallback label. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| # about collisions/inversion/preimages of the hash function itself — so the keccak trigger | ||
| # requires the word near "slot"/"storage", the access-shaped usage this gate targets. | ||
| _KECCAK_SLOT = r"keccak\S*.{0,40}(?:slot|storage)|(?:slot|storage).{0,40}keccak" | ||
| _SKIP_GATE_CHECKS: list[tuple[re.Pattern[str], str, re.Pattern[str]]] = [ |
There was a problem hiding this comment.
claude, have you THOUGHT LONG AND HARD ABOUT THIS?
The record_skip gate no longer does NLP-in-regex over LLM prose (_SKIP_GATE_CHECKS/_KECCAK_SLOT are gone). The author instead self-classifies every skip via a required reason_category Literal on the tool schema; the gate is purely structural: a storage_access skip cannot be recorded until alternatives_considered is non-empty. The judge polices classification honesty — the category is surfaced in its input and Criteria 7 instructs it to audit access-shaped reasons categorized otherwise as storage_access. SkippedProperty defaults the category to "other" for cache back-compat, and only non-default categories join _compute_digest (the same empty-contributes-nothing scheme as alternatives_considered), so cached pre-field skips keep their legacy digests and validation stamps. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The mirrored NotRequired harness_augmentation fields on FeedbackInherentParams/JudgeSystemParams (aligned only by comments) are gone. property_feedback_judge now takes harness_augmentation: bool = False and injects it into BOTH the task-prompt and system-prompt binds via one helper, so the two judge templates cannot disagree; a pipeline that gains harness augmentation flips exactly one Python argument. The templates' default(false) remains as a render fail-safe only. Render tests assert the harness wording is present/absent in both templates together for flag true and false, including through the _bind_harness_augmentation injection path itself. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Reworked per the architecture review — delta since the previous version (aef046a → b7cf58e):
|
| key compromising, phishing, etc.) | ||
| 2. Reasoning about hash function behavior or hash collisions (e.g., | ||
| "invalid signatures should be rejected") | ||
| 2. Reasoning about hash function *collisions or inversion* (e.g., |
There was a problem hiding this comment.
We need to see how this updated section interacts with proper erc-7201 annotations. It may apply in cases where proper annotations do not exist or that we failed to parse them.
| If the property being skipped could be formalized with Solidity code changes, the skip is VALID. Do *NOT* reject | ||
| skips with advice to change the Solidity code. The Solidity code is *IMMUTABLE* and suggestions to change the Solidity | ||
| code are INVALID. | ||
| If the property being skipped could be formalized only with *protocol* Solidity code changes, the skip is VALID. |
There was a problem hiding this comment.
to adapt to John's edit agent
|
|
||
| {% if sort != "greenfield" %} | ||
| ## No Source Changes | ||
| ## No Protocol Source Changes |
There was a problem hiding this comment.
n.b. protocol source changes means we do not change the original files, but source changes involving harnesses are allowed
| "to READ from CVL (private/internal fields, unstructured or keccak-addressed storage, " | ||
| "no public getter); `hash_collision` — the property requires reasoning about " |
There was a problem hiding this comment.
again, private/internal fields are eminently readable from cvl via direct storage access, and this tool text will land in the attention window of the agent before it ever sees the CVL research articles about this being possible. This will make the problem worse, not better, the agent will start out thinking that private/internal storage fields can't be read and this is a problem to be overcome.
| "The alternative CVL/verification mechanisms you considered before skipping, and why " | ||
| "each does not work for this property. Required (non-empty) when `reason_category` is " | ||
| "`storage_access`, where you MUST address the applicable mechanisms among: precomputed " | ||
| "keccak storage-slot constants (keccak256 is a built-in CVL function), Sload/Sstore " |
There was a problem hiding this comment.
this is true (keccak256 is built in), but doing keccak256("whatever.namespace") in cvl won't yield the same value as the (likely precomputed) constant generated by the solidity compiler. Further, there's no way to do storage[keccak256("whatever")], the only way keccak256 would help you here is if you wrote a raw storage acccess hook and did conditional branching on slot == keccak256("..."). But, again, this doesn't work, because the value computed by keccak256 doesn't actually match whatever concrete value is being computed by the solidity compiler.
There was a problem hiding this comment.
the solution is to allow munging/editing around unstructured storage so our existing (and quite robust!) support for eip-7201 makes this problem go away.
For reference, on some project (I forget which), once I got the eip-7201 annotations in the right place (in autosetup) the cvl author was able to use the names we generated just fine.
So: I agree we should reject skips around unstructured storage, but the fix mechanism does not (and cannot) exist spec side.
| (c) ghost state mirroring the value via storage hooks or function summaries. | ||
|
|
||
| The author self-classifies each skip with a reason category (shown alongside the skip): if the | ||
| reason is access-shaped — state described as private, unstructured, keccak-addressed, or lacking |
There was a problem hiding this comment.
I will note the "private" restriction is, again, totally invented by claude here, and trust all future such "limitations" are cleaned up rather than continuing to point them out
| Do *NOT* reject skips with advice to change the protocol Solidity code. The *protocol* Solidity code is | ||
| *IMMUTABLE* and suggestions to change the protocol code are INVALID. Verification *harnesses* (contracts under | ||
| `certora/harnesses/`) are NOT protocol code: | ||
| {% if harness_augmentation | default(false) %} | ||
| needing a harness getter or wrapper (mechanism (b) above) does NOT make a skip valid under this rule. | ||
| {% else %} | ||
| a skip needing only a harness getter or wrapper is NOT validated by this rule — it falls under the | ||
| "missing harness support" labeling above. | ||
| {% endif %} |
There was a problem hiding this comment.
this advice will collide with my editor agent advice soon
| slots are compile-time constants. The available mechanisms are: | ||
|
|
||
| * **Slot-constant `definition`s**: compute the concrete 32-byte slot value *once* (from the constant declared | ||
| in the source, or by evaluating the keccak expression yourself) and bind it with a CVL `definition`, e.g., |
There was a problem hiding this comment.
evaluating the keccak expression yourself
We think claude can reliably simulate the sha3 algorithm???
There was a problem hiding this comment.
Not sure about the API we use but the chatbots get it right. But those are probably highly engineered behind the scene to write sandboxed code and execute it
Part 1 of 6 related spec-quality PRs derived from comparing generated specs against hand-written specs from a recent customer engagement (generated specs skipped properties claiming keccak-derived storage slots are unreachable in CVL — they are not).
What
prop_inference.pyCERTORA_BACKEND_GUIDANCE: hash collision/inversion reasoning remains out of scope, but keccak-derived storage slots are compile-time constants — hookable viaSload/Sstoreand exposable via harness getters.property_generation_prompt.j2: new<storage_access>block (slot-constant definitions, slot hooks, ghost mirroring, harness getters, KB pointers).property_judge_system_prompt.j2: "No Source Changes" → "No Protocol Source Changes"; harnesses are verification infrastructure. Harness-augmentation wording is injected byproperty_feedback_judge(harness_augmentation: bool = False)into both judge templates through one bind helper (no-op until the main-harness PR flips the argument).property_judge_prompt.j2Criteria 7: judge must check slot-hook / harness-getter / ghost+summary expressibility before accepting a "not expressible" skip, and audits access-shaped skip reasons whose declared category isn'tstorage_access; greenfield keeps unconditional rejection (author controls the stubs).cvl_generation.py_RecordSkipSchema: requiredreason_category: Literal["storage_access", "hash_collision", "prover_limitation", "environment", "other"]plus requiredalternatives_considered. The gate is purely structural: astorage_accessskip cannot be recorded untilalternatives_consideredis non-empty — no text matching over LLM prose. Category and alternatives are surfaced verbatim to the judge and covered by the validation digest (mutating them stales the stamp; default"other"category and empty alternatives hash like legacy for cache back-compat).Design revisions after architecture review
_SKIP_GATE_CHECKS/_KECCAK_SLOT) deleted — NLP-in-regex over LLM prose was brittle; the author now self-classifies via thereason_categoryenum and the gate checks structure only.storage_access(judge polices semantics, not a regex).reason_categoryjoins_compute_digestonly when non-default, so cached pre-field skips keep their legacy digests and stamps.NotRequiredharness_augmentationfields onFeedbackInherentParams/JudgeSystemParams(aligned only by comments) removed — the flag now threads once throughproperty_feedback_judgeinto both template binds, so the two judge prompts cannot disagree.Cross-PR interactions
harness_augmentationis flipped to true by the ap-main-harness PR (Main-contract augmentation harness: unstructured-storage getters, helper decomposition, verify-target threading #40) changing exactly one Python argument at theproperty_feedback_judgecall site; safe to merge in either order.reason_category+ structural gate + digest coverage + judge-audit design is the shared "structured exception ledger" pattern that Vacuity detection + repair ladder: fix setup root causes instead of filtering methods #41 (vacuity repair) reuses foracknowledge_vacuous_method; Vacuity detection + repair ladder: fix setup root causes instead of filtering methods #41 rebases on this PR's_compute_digestand judge-input plumbing.property_generation_prompt.j2/property_judge_prompt.j2in disjoint sections from ap-vacuity-repair and ap-cvlmath; later-merging PR rebases mechanically.Tests
uv run pytest tests/ -q -m "not expensive" --ignore=tests/test_rag_db.py --ignore=tests/test_token_usage.py→ 137 passed, 3 skipped, 3 deselected (ignored files fail identically on master: Postgres testcontainer / model-registry env).tests/test_cvl_skips.pyrewritten for the enum gate (structural gating, digest back-compat, judge surfacing);tests/test_template_render.pygained the cross-template agreement tests. Pyright on changed files: 0 errors.🤖 Generated with Claude Code