Skip to content

Encode decode property test - #16964

Draft
msooseth wants to merge 39 commits into
developfrom
encode_decode_property_test
Draft

msooseth wants to merge 39 commits into
developfrom
encode_decode_property_test

Conversation

@msooseth

@msooseth msooseth commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Property tests for ABI encode & decode loop.

Checklist

AI Disclosure

  • No AI tools were used
  • Total YOLO, barely any review

Will work more on it in the coming days.

msooseth and others added 30 commits September 10, 2026 13:43
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
msooseth force-pushed the encode_decode_property_test branch from 02817ab to 7ee1bba Compare September 10, 2026 16:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant