-
Notifications
You must be signed in to change notification settings - Fork 4
Stop false 'keccak/unstructured storage is unreachable' skips; gate skip justifications #39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
b6e5e7a
0f62a86
2b430e5
c5a45cd
aef046a
b439d33
b7cf58e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -46,10 +46,35 @@ def feedback(self) -> str: ... | |
| # Feedback types | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
| # The author's self-classification of a skip reason. ``storage_access`` names the one | ||
| # category that is a capability gap CVL can bridge (keccak slot constants, storage hooks, | ||
| # harness getters, ghost mirroring) rather than an expressiveness limit, so it is the only | ||
| # category the record_skip gate treats specially. The judge polices classification honesty | ||
| # (Criteria 7 in property_judge_prompt.j2 audits access-shaped reasons categorized | ||
| # otherwise as storage_access), so the enum is a declaration, not a trusted fact. | ||
| ReasonCategory = Literal[ | ||
| "storage_access", # the state exists on-chain but is hard to *read* from CVL | ||
| "hash_collision", # requires reasoning about collisions/preimages/inversion of a hash | ||
| "prover_limitation", # a documented prover limit (e.g. quantifying over unbounded arrays) | ||
| "environment", # depends on off-chain/environment behavior CVL cannot model | ||
| "other", | ||
| ] | ||
|
|
||
|
|
||
| class SkippedProperty(BaseModel): | ||
| """A property the agent explicitly decided not to formalize.""" | ||
| property_title: str = Field(description="The unique snake_case title of the property from the batch listing") | ||
| reason: str = Field(description="Justification for why this property was skipped") | ||
| # Defaulted for back-compat with cached instances that predate the field. | ||
| alternatives_considered: list[str] = Field( | ||
| default_factory=list, | ||
| description="The alternative mechanisms the author considered before skipping, and why each does not work", | ||
| ) | ||
| # Defaulted for back-compat with cached instances that predate the field. | ||
| reason_category: ReasonCategory = Field( | ||
| default="other", | ||
| description="The author's self-classification of the skip reason", | ||
| ) | ||
|
|
||
|
|
||
| class PropertyRuleMapping(BaseModel): | ||
|
|
@@ -152,6 +177,18 @@ def _compute_digest(curr_spec: str, skipped: list[SkippedProperty]) -> str: | |
| digester.update(curr_spec.encode()) | ||
| for s in skipped: | ||
| digester.update(f"{s.property_title}:{s.reason}".encode()) | ||
| # The judge audits reason_category as part of the skip's justification | ||
| # (feedback.py surfaces it), so changing it must stale the validation stamp. | ||
| # The default "other" contributes nothing, so digests of cached pre-field | ||
| # skips (which deserialize with the default) keep their legacy value. | ||
| if s.reason_category != "other": | ||
| digester.update(f"|cat:{s.reason_category}".encode()) | ||
| # The judge audits alternatives_considered as part of the skip's justification | ||
| # (feedback.py surfaces them), so changing them must stale the validation stamp. | ||
| # An empty list contributes nothing, so digests of cached pre-field skips (which | ||
| # deserialize with the default empty list) keep their legacy value. | ||
| for alt in s.alternatives_considered: | ||
| digester.update(f"|alt:{alt}".encode()) | ||
| return digester.hexdigest() | ||
|
|
||
|
|
||
|
|
@@ -305,14 +342,24 @@ async def run(self) -> Command: | |
| ) | ||
| return tool_state_update(self.tool_call_id, msg) | ||
|
|
||
| # Skip gate: the author self-classifies each skip via ``reason_category``. A | ||
| # ``storage_access`` skip ("the state is hard to *read*") names a capability gap that CVL | ||
| # can bridge — precomputed keccak slot constants, Sload/Sstore hooks, harness getters, | ||
| # ghost mirroring — not an expressiveness limit, so it cannot be recorded until | ||
| # ``alternatives_considered`` is non-empty. The gate is purely structural (enum value + | ||
| # non-empty field check); the judge audits the *substance* of the alternatives and | ||
| # polices classification honesty (an access-shaped reason categorized otherwise is | ||
| # audited as storage_access — see Criteria 7 in property_judge_prompt.j2). | ||
|
|
||
| @tool_display( | ||
| lambda p: f"Skipping property `{p.get('property_title', '?')}`", | ||
| suppress_ack("Skip result", ("Recorded skip",)), | ||
| ) | ||
| class _RecordSkipSchema(WithInjectedState[CVLGenerationState], WithInjectedId, WithImplementation[Command]): | ||
| """ | ||
| Declare that you are skipping a property from the batch. | ||
| You must provide the property's title and a justification. | ||
| You must provide the property's title, a justification, a category classifying | ||
| the justification, and the alternative mechanisms you considered before skipping. | ||
| The feedback judge will evaluate whether your justification is valid. | ||
| Only use this after genuinely attempting to formalize the property. | ||
| """ | ||
|
|
@@ -322,6 +369,28 @@ class _RecordSkipSchema(WithInjectedState[CVLGenerationState], WithInjectedId, W | |
| reason: str = Field( | ||
| description="Justification for why this property cannot be formalized" | ||
| ) | ||
| reason_category: ReasonCategory = Field( | ||
| description=( | ||
| "Classify the skip reason: `storage_access` — the state exists on-chain but is hard " | ||
| "to READ from CVL (private/internal fields, unstructured or keccak-addressed storage, " | ||
| "no public getter); `hash_collision` — the property requires reasoning about " | ||
| "collisions, preimages, or inversion of a hash function itself; `prover_limitation` — " | ||
| "a documented prover limit (e.g. quantifying over arbitrary-length arrays); " | ||
| "`environment` — depends on off-chain/environment behavior that cannot be modeled; " | ||
| "`other` — anything else. The feedback judge audits this classification: an " | ||
| "access-shaped reason categorized as anything but `storage_access` will be re-audited " | ||
| "as `storage_access`." | ||
| ) | ||
| ) | ||
| alternatives_considered: list[str] = Field( | ||
| description=( | ||
| "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 " | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is true (
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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. |
||
| "storage hooks, harness getters/helpers, and ghost mirroring of contract state." | ||
| ) | ||
| ) | ||
|
|
||
| @override | ||
| def run(self) -> Command: | ||
|
|
@@ -336,9 +405,25 @@ def run(self) -> Command: | |
| self.tool_call_id, | ||
| "A non-empty justification is required when skipping a property.", | ||
| ) | ||
| if self.reason_category == "storage_access" and not any( | ||
| alt.strip() for alt in self.alternatives_considered | ||
| ): | ||
| return tool_state_update( | ||
| self.tool_call_id, | ||
| "Skip REJECTED: a `storage_access` skip names a capability gap CVL can bridge, " | ||
| "not an expressiveness limit. Before this skip can be recorded, " | ||
| "`alternatives_considered` must address the applicable mechanisms among: a " | ||
| "precomputed keccak storage-slot constant (keccak256 is a built-in CVL function); " | ||
| "Sload/Sstore storage hooks on the slot/path in question; a harness getter/helper " | ||
| "or direct storage access (currentContract.<field>); ghost state mirroring the " | ||
| "value via hooks or summaries. Either attempt the applicable mechanism, or explain " | ||
| "in `alternatives_considered` why it does not apply to this property.", | ||
| ) | ||
| skip = SkippedProperty( | ||
| property_title=self.property_title, | ||
| reason=self.reason, | ||
| reason_category=self.reason_category, | ||
| alternatives_considered=self.alternatives_considered, | ||
| ) | ||
| return tool_state_update( | ||
| self.tool_call_id, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -88,8 +88,15 @@ class RefinementState(MessagesState): | |
|
|
||
| 1. Attack vectors or invariants that reference off-chain events (like | ||
| 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., | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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. |
||
| "invalid signatures should be rejected"). NB: this exclusion is | ||
| narrow. `keccak256` is a built-in CVL function, and storage-slot | ||
| constants derived from it (ERC-7201 namespaces, EIP-1967 proxy | ||
| slots, `keccak256(name) - 1` patterns) are compile-time constants | ||
| that can be precomputed and hooked via `Sload`/`Sstore` slot hooks. | ||
| Unstructured storage is likewise reachable via such hooks or via | ||
| harness getters. NEVER reject a property merely because the state | ||
| it references lives in keccak-derived storage slots. | ||
| 3. Event emission (not impossible, simply difficult and tedious) | ||
|
|
||
| In addition, due to the advent of checked arithmetic, properties that | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -107,6 +107,27 @@ within the generated summary, match the callee contract against known implemente | |
| For those callees, simply dispatch the call to the relevant implementation. For calls "outside" of the prover | ||
| inputs, fall back on a sound model of the external behavior, using e.g., ghosts. | ||
|
|
||
| <storage_access> | ||
| Never conclude that contract state is "unreachable" (and never skip a property on that basis) merely because | ||
| the state lives in keccak-derived storage slots (ERC-7201 namespaced storage, EIP-1967 proxy slots, | ||
| `keccak256(name) - 1` patterns) or lacks a public getter. `keccak256` is a built-in CVL function, and these | ||
| 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., | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
We think claude can reliably simulate the sha3 algorithm???
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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 |
||
| `definition IMPLEMENTATION_SLOT() returns uint256 = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;` | ||
| * **`Sload`/`Sstore` hooks**: hook reads/writes of the storage in question to observe or constrain its value. | ||
| For state reached through named storage paths, use the named-path hook patterns with `KEY`, e.g., | ||
| `hook Sstore _balances[KEY address user] uint256 newVal { ... }`. | ||
| * **Ghost mirroring**: declare a ghost variable updated by an `Sstore` hook (and constrained by the matching | ||
| `Sload` hook), then reference the ghost in your rules/invariants. | ||
| * **Harness getters**: as a fallback, if the verification setup includes harness contracts, their external | ||
| view getters expose otherwise-private/unstructured state directly. | ||
|
|
||
| Before concluding that any state is unreachable, search the knowledge base for "keccak storage slot" and | ||
| "unstructured storage hook" — these entries document the exact hook grammar and worked examples. | ||
| </storage_access> | ||
|
|
||
| ## Step 4 | ||
|
|
||
| Once all non-skipped properties have been approved by the feedback judge, AND all rules that are not expected | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -168,14 +168,57 @@ Verify that the specification covers all {{ properties | length }} listed proper | |
| perform your own research (or examine any CVL Document references cited in their skip) | ||
| and critically examine their claims about CVL expressivity when evaluating the skip. | ||
|
|
||
| Before accepting ANY skip justified as "not expressible in CVL" — in particular one claiming that state is | ||
| private, unstructured, keccak-addressed, or lacks a getter — you must verify that NONE of the following | ||
| mechanisms make the property expressible: | ||
| (a) a precomputed keccak storage-slot constant (`keccak256` is a built-in CVL function; ERC-7201 namespaces | ||
| and EIP-1967 slots are compile-time constants) combined with an `Sload`/`Sstore` hook on that slot; | ||
| (b) a harness getter or direct storage access (e.g., `currentContract.<field>`); | ||
| (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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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 |
||
| a getter — but categorized as anything other than `storage_access`, audit it as `storage_access` | ||
| anyway, running the full mechanism checklist above; the category is the author's claim, not a fact. | ||
| {# harness_augmentation is injected by property_feedback_judge (composer/spec/feedback.py) into both | ||
| this template's bind and property_judge_system_prompt.j2's bind from one function parameter, so | ||
| the two templates always agree; default(false) is a render fail-safe only. Only pipelines that | ||
| can (re)generate main-contract augmentation harnesses may instruct the judge to reject | ||
| harness-shaped skips; everywhere else (the false default) | ||
| such skips are accepted with the "missing harness support" label the system prompt mandates. | ||
| Greenfield is checked first because harness pipelines (and the immutability rule the fallback | ||
| compensates for) only exist for pre-existing source: the greenfield author shapes the stub's API | ||
| themselves, so a missing getter is never a capability gap there. #} | ||
| {% if sort == "greenfield" %} | ||
| If any of these mechanisms applies, reject the skip and name the applicable mechanism. There is no | ||
| pre-existing implementation here: the author defines the contract API through the stub, so a skip | ||
| claiming state lacks a getter is invalid — reject it and advise exposing the state through a getter | ||
| in the stub. | ||
| {% elif harness_augmentation | default(false) %} | ||
| If any of these mechanisms applies, reject the skip and name the applicable mechanism. | ||
| {% else %} | ||
| If mechanism (a) or (c) applies, or (b) applies via direct storage access, reject the skip and name the | ||
| applicable mechanism. If the property is expressible ONLY through a new harness getter or wrapper — | ||
| a mechanism that cannot be generated in this pipeline — do NOT reject the skip on those grounds; | ||
| accept it, but label it as "missing harness support, not CVL inexpressibility". Never endorse a claim | ||
| that such a property is beyond CVL's expressiveness. | ||
| {% endif %} | ||
|
|
||
| *Exception*: if the author has filed an empirical rebuttal (see Step 1) against a prior-round | ||
| formalization you proposed, accept that rebuttal per the Step 1 evaluation rules — do not | ||
| re-insist on the same proposal. If the rebuttal establishes that no working formalization was | ||
| found, the skip is VALID on the grounds that your prior proposal proved unworkable. | ||
| {% if sort != "greenfield" %} | ||
| 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. | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. to adapt to John's edit agent |
||
| 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 %} | ||
|
Comment on lines
+213
to
+221
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this advice will collide with my editor agent advice soon |
||
| {% endif %} | ||
|
|
||
| **Array quantification limitation**: The Certora Prover cannot universally quantify over | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.