Support verifying a library main contract via a generated harness - #132
Draft
shellygr wants to merge 3 commits into
Draft
Support verifying a library main contract via a generated harness#132shellygr wants to merge 3 commits into
shellygr wants to merge 3 commits into
Conversation
The Prover skips libraries when instantiating parametric rules, so verifying a library as the main contract silently proves nothing. There is no error to key on; the run just comes back vacuous. certora_autosetup/harnesser generates a plain contract exposing one public wrapper per library function, which the caller verifies instead: - detect: solc --standard-json with stopAfter "parsing" reads contractKind without resolving imports or building, so detection costs milliseconds. - run: emit a placeholder harness, probe-build it alongside the library, read the library's API, then refill the file. The library is listed in the build's files explicitly, because an imported-but-unused library is not compiled as its own contract and the build then reports none of its functions. The placeholder carries one external function since a method-less contract is dropped by contract discovery and by the signature database. - read_build: the API comes from allMethods (external + internal + private), not internalFunctions, which holds autofinder instrumentation and is systematically empty for libraries. A library is located by (name, file): Solady ships 17 library names twice with differently scoped structs. - plan: classify each function, own a state variable per storage receiver, escape CVL keywords before mangling ABI collisions, derive storage readers, and synthesize a return for in-place memory mutators. render_wrapper_contract grows a parentless mode (a library cannot be a base contract) and a slot for file-scoped pragmas. Measured against the corpora, matching hardhat-exposed's independent counts where they overlap: OZ Math 14/14, EnumerableSet 18/24, Checkpoints 20/33, StorageSlot refused (all 8 return storage pointers); Solady LibBitmap 10/10, LibSort 57/68, EnumerableSetLib 45/51. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both entry points swap the main contract before anything derives state from its name. The name reaches the sanity spec, the base conf, the verify target and the result keys, so swapping later would leave those naming a contract the Prover instantiates no methods against. - autosetup CLI swaps right after the main handle is parsed. - composer swaps ahead of SourceFields, so component analysis, CVL authoring and the conf's verify target all agree on one name — AutoSetup keys its returned summary and config by that name too. The library stays in the scene beside the harness: the wrappers call into it, and the build only reports a library's own functions when it is named as a file in its own right. The LLM harness agent owns certora/harnesses and rewrites every entry it is given, so it now skips files carrying the generated-harness sentinel — that file is the verification target, not a wrapper to be improved on. Verified end to end: autosetup on OZ EnumerableSet emits a conf whose verify is CertoraLibraryHarness_EnumerableSet and whose files list both the harness and the library. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every storage receiver belongs to the library being harnessed, so repeating its name in each identifier only made them long: at_EnumerableSet_Bytes32Set and certoraStore_EnumerableSet_Bytes32Set_inner_indexes where the hand-written harness beside it writes at_ and _indexOf. Names now read at_Bytes32Set, length_Bytes32Set, _certoraStore_Bytes32Set and Bytes32Set_inner_indexes. A type from outside the library keeps its qualifier, which is what still tells it apart. Readers are named for the type and member path rather than the state variable, so they read as accessors. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
shellygr
commented
Aug 10, 2026
| from certora_autosetup.harnesser.run import ensure_library_harness | ||
|
|
||
|
|
||
| def _split_target(target: str) -> tuple[str, str]: |
Contributor
Author
There was a problem hiding this comment.
to Claude: I'm surprised we don't have such a utility function yet
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The Prover cannot verify a
libraryas the verification target, and fails silently — there is no error anywhere in certora-cli or the JVM.CalculateMethodParamFilters.kt:182filters libraries out of parametric rules, so the run reaches the prover, instantiates zero methods, and comes back vacuous.The Prover itself prescribes the fix: "Calling Library functions from spec is not supported. Use a harness function that calls the library one if needed." This automates it.
How
New
certora_autosetup/harnesser/package. Everything completes beforeAutosetup.runstarts:Three findings shaped this, each verified against real builds:
files. An imported-but-unused library is not compiled as its own contract — the build reports its structs and none of its functions.allMethods, notinternalFunctions. The latter is autofinder instrumentation, and certora-cli skips autofinders for library-hosted functions (certoraBuild.py:2751), so it is systematically empty for libraries. Reading it yields zero wrappers for a library whose surface is all-internal.setup_prover.py:327re-seeds from the base build-system dict, so a secondrun_compilation_analysiswould discard every workaround from the first; and a swap insidesetup_provercannot propagate (it returns no handle,ContractHandleis frozen).Detection uses
solc --standard-jsonwithstopAfter: "parsing"— real AST, no build, no import resolution, milliseconds. It keys oncontractKind, never on a message, since there is no message.Codegen
Storage receivers become harness-owned state; CVL keywords are escaped before ABI-collision mangling (
at→at_, then suffixed); collision suffixes come from the dropped receiver keyed positionally (overloads share a name); storage readers reach through nested structs and mappings; in-place memory mutators get a synthesized return.Skipped and reported, not silently dropped: private, storage-pointer returns, internal-only types, opaque
bytes32 ptrhandles. Zero surviving wrappers is a hard error — a method-less target proves nothing.Coverage
Matches
hardhat-exposed's independent counts where they overlap:pushutils/andg/Validation
Full local autosetup on OZ EnumerableSet: emits a conf whose
verifyandparametric_contractsareCertoraLibraryHarness_EnumerableSet, typechecker passes, local prover run succeeds,certoraRun --compilation_steps_onlyexits 0. A/B against OpenZeppelin's hand-writtenEnumerableSetHarness.sol— the generated names now readadd/remove/contains/at_Bytes32Set/Bytes32Set_inner_indexesagainst the human'sadd/remove/contains/at_/_indexOf; the remaining differences are the two extra set types the human harness does not cover.Tests live in the Autosetup repo (29 unit tests over real trimmed build fixtures); needs a submodule pin bump there once this merges.
Known limitation
Solady's
EnumerableSetLibsets arestruct { uint256 _spacer; }with assembly-derived slots, so the owned state variable may not model where the data actually lives. Treat its 45/51 as unproven until a prover run confirms it.🤖 Generated with Claude Code