Conversation
Draws a random ABI type together with the canonical encoding of a random value of that type, computed here from the ABI specification, and requires contracts generated for the type to hand that encoding back unchanged. Two properties: one that runs `abi.encode(abi.decode(input, (T)))` inside a single source unit across coder v1/v2, legacy/IR codegen and optimiser settings, and one that additionally routes the value through an external call to a second source unit, which may use the other coder version. Compilations that run out of stack slots or hit an unimplemented legacy codegen path are skipped, since they say nothing about the coders. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0135HmdbvcLky2zE9UsbRWb3
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0135HmdbvcLky2zE9UsbRWb3
Adds the value types that were missing: enums (only declared members are valid, so values stay in range), contract types and user-defined value types. All 32 widths of uintN/intN/bytesN are now generated rather than a sample of them, since that is where cleanup differs. The top level is now a tuple of one to three components instead of always a single value, so head/tail composition is exercised at the top level too and `abi.encode`/`abi.decode` are used with several arguments. External function types stay out: `abi.decode`'s type list does not parse them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0135HmdbvcLky2zE9UsbRWb3
The counters were meant to be temporary and got committed by accident. They also hung the process on exit: the reporter iterated a function-local static map that had already been destroyed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0135HmdbvcLky2zE9UsbRWb3
Measuring what the domains produced showed the padding paths were never reached: over 5185 cases `bytes` was generated zero times, `string` four times, and the only payload lengths ever produced were 1 and 2. Two causes. `ElementOf` over one flat list of ~100 elementary types gave `bytes` and `string` two entries between them, most of the rest being the 66 user-defined value types. The leaf domain now draws from four groups of equal weight, so the two types whose encoding needs padding are no longer a rounding error. `VectorOf(...).WithMaxSize()` grows a container gradually as the fuzzer mutates, and within a one second burst it never got past two elements. Both byte-string payloads and dynamic array lengths now draw their length up front, byte strings from a domain that includes 0, 31, 32, 33, 63, 64, 65, 95, 96 and 97 explicitly. The same run now produces 6928 payloads, 4564 of them unaligned, covering 30 of the 32 residues mod 32 and every boundary length. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0135HmdbvcLky2zE9UsbRWb3
The oracle in this test is hand-written, so a mistake in it looks exactly like a compiler bug. These guards make that distinction visible. Each `AbiType` node states what `width` and `components` may hold for its kind: no components on a leaf, exactly one element type on an array, a non-zero length on a fixed array, at least one member on a struct, an elementary value type under a user-defined one, and so on. They are checked both when a node is built and whenever one is inspected. The encoder checks its own output the same way: every component encoding is a whole number of words, a static type's encoding fills exactly its head slot, and the head ends up the size the head sizes predicted. A component that is off by a few bytes would otherwise silently shift the whole tail. `ABICoderTypeInvariants.MalformedTypesAreRejected` covers the guards themselves, so they cannot quietly stop checking. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0135HmdbvcLky2zE9UsbRWb3
The prose said the same thing three ways. Replaced the surplus with the generated source unit and the encoding for one concrete tuple, which answers the two questions the file otherwise leaves open: where the contract comes from, and where the random value lives. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0135HmdbvcLky2zE9UsbRWb3
The example tuple had no struct in it, so the source unit the import refers to was never shown. Swapped `bytes` for a struct wrapping one, which gives types.sol something to hold and makes the encoding show a nested offset, counted from the start of the struct rather than of the whole encoding. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0135HmdbvcLky2zE9UsbRWb3
Every generated source unit now pins abicoder v2, so the Coder enum, coderPragma() and both supportedByCoderV1() overloads go, along with the clamping that forced v2 for via-IR and for types v1 cannot express. CrossCoderCallRoundTripIsIdentity becomes CallRoundTripIsIdentity, since there is no longer a coder pairing to vary. Under v2 the legacy pipeline short-circuits into the same ABIFunctions coder that via-IR uses, so compiling both ways no longer varies the code under test -- it only varied v1's separate hand-written legacy coder. Compile via-IR only, which removes CompilationSettings and the whole `unsupported` escape hatch: no StackTooDeep/UnimplementedFeature catches and no silently skipped inputs. Canonical value generation stays as is; the v2 decoder validates, so dirty higher-order bits would revert rather than round trip. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0135HmdbvcLky2zE9UsbRWb3
The test carried its own implementation of the ABI specification -- head/tail layout, tail offsets, padding, sign extension -- so that it could hand the contract a canonical encoding to decode. That made the property "solc's encoding equals the spec", at the price of ~300 lines that have to be right, where a bug is a spurious failure. The value now comes from a tape of raw fuzzer bytes that the generated contract reads it off, and the property is that decoding an encoding and encoding it again reproduces it byte for byte. The contract exposes `encodeValue`, which builds the tuple off the tape and returns `abi.encode` of it, and `renormalize`, which decodes that and encodes it once more; the test only compares the two blobs and never looks inside either. A builder that got a type wrong builds a different value, never a failure, and the sources still depend only on the tuple's types, so one compilation keeps serving every tape drawn for it. Stack-too-deep from the via-IR code generator now skips the input instead of escaping as an uncaught exception. It is a codegen limit rather than an ABI coder result, and it aborted the run within a minute of fuzzing before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0135HmdbvcLky2zE9UsbRWb3
The fixed Solidity block was spelled as escaped string literals, one line per concatenation, which made it hard to read as the code it is. It is now a raw literal rendered through Whiskers, matching how the codegen writes its templates. The table of byte-string lengths is indexed modulo `lengths.length` instead of a repeated literal 12. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0135HmdbvcLky2zE9UsbRWb3
msooseth
force-pushed
the
encode_decode_property_test
branch
from
September 10, 2026 16:29
02817ab to
7ee1bba
Compare
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.
Property tests for ABI encode & decode loop.
Checklist
AI Disclosure
Will work more on it in the coming days.