Skip to content

fix(uplc): reject constr/case before 1.1.0 and constr tags of 2^64 or more - #702

Merged
solidsnakedev merged 3 commits into
IntersectMBO:mainfrom
easy1staking-com:fix/uplc-constr-case-version-gate
Oct 28, 2026
Merged

solidsnakedev merged 3 commits into
IntersectMBO:mainfrom
easy1staking-com:fix/uplc-constr-case-version-gate

Conversation

@nemo83

@nemo83 nemo83 commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

Fixes #701.

Plutus 1.72.0.0 decodeTerm (UntypedPlutusCore/Core/Instance/Flat.hs, handleTerm 8 line 167 and handleTerm 9 line 181) rejects constr and case in a program below version 1.1.0. It reads the constr tag as a Word64 with dWord64 (PlutusCore/Flat/Decoder/Strict.hs line 165), which allows at most ten 7-bit chunks with a tenth chunk of 0 or 1. UPLC.ts checked neither, in either direction.

Changes

  • decodeTerm and encodeTerm take the program version. Before reading or writing constr or case, they check it with Plutus's ordering (major, then minor, then patch) and fail with Plutus's message: 'constr' is not allowed before version 1.1.0, this program has version: 1.0.0.
  • The constr tag is decoded with a new decodeWord64 that follows dWord64. The encoder refuses tags above 2^64-1. A negative tag was already refused by encodeNatural.
  • decodeVersion and encodeVersion read and write each version component as a Flat natural, as Plutus's instance Flat Version does (PlutusCore/FlatInstances.hs), instead of one byte. Without this, the encoder's version check could be bypassed: 257.0.0 was truncated to 1.0.0. Versions with every component below 128 encode to the same bytes as before.
  • The public API is unchanged. Only internal function signatures changed.
  • Patch changeset for @evolution-sdk/evolution.

Tests

New tests in packages/evolution/test/UPLC.test.ts:

  • Each of the conformance files constr/constr-06.flat, constr/constr-08.flat, constr/constr-09.flat and case/case-07.flat (bytes copied in) now fails to decode with a UPLCError. For constr-09, the error is the version one.
  • Boundaries that still pass: constr/constr-01.flat, case/case-01.flat and constr/constr-07.flat (tag 2^64-1) decode and re-encode byte for byte. constr is accepted in a 2.0.0 program, and a tag spread over exactly ten chunks is accepted.
  • Versions: 257.0.0 encodes as 810200008001 and decodes back, and (program 0.255.0 (constr 0)) (00ff01008001) is rejected with the version error.
  • Also rejected: a tag spread over eleven chunks, constr nested under a lambda in a 1.0.0 program, and encoding constr/case at 1.0.0 or a tag of 2^64.

The new rejection tests fail on main and pass with the fix. With each guard reverted on its own (decode or encode version gate, Word64 decode, Word64 encode), at least one test fails.

Across all 908 .flat files in plutus-conformance/test-cases/uplc (Plutus 1.72.0.0), decode then re-encode changes outcome for exactly these four files, from accepted to rejected.

🤖 Generated with Claude Code

nemo83 and others added 3 commits October 21, 2026 15:14
… more

Plutus 1.72.0.0 decodeTerm (UntypedPlutusCore/Core/Instance/Flat.hs) fails
on a constr or case term in a program below version 1.1.0, and reads the
constr tag with dWord64, which fails past ten 7-bit chunks or a value of
2^64 or more. UPLC decoded and encoded both. The decoder now applies the
same checks, and the encoder refuses to write such terms.

Conformance cases constr-06, constr-08, constr-09 and case-07 are now
rejected; no other conformance file changes outcome.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Match dWord64's "Unexpected extra byte in unsigned integer", say in the changeset
that an overlong tag is rejected even when its value fits, and assert the
2^64-1 tag with toMatchObject.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Plutus encodes each Version component as a Flat Natural (PlutusCore/FlatInstances.hs),
but UPLC used one byte per component. A component of 256 or more was truncated, so
Program({ version: "257.0.0", body: constr 0 }) was written as a 1.0.0 constr program,
bypassing the new version gate; correctly encoded versions with a component of 128 or
more were misread. Components below 128 encode to the same bytes as before.

The changeset now separates what decoding rejects (overlong Word64 encodings too)
from what the encoder refuses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nemo83
nemo83 requested a review from solidsnakedev as a code owner October 21, 2026 20:53

@solidsnakedev solidsnakedev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this matches Plutus. The version gate, the Word64 tag and the version naturals all hold against Flat.hs:167-184, Strict.hs:165-184 and FlatInstances.hs:177-179 at 1.72.0.0. Every program in the repo re-encodes to the same bytes. Of the 908 conformance files, only constr-06, constr-08, constr-09 and case-07 change, and all four are now rejected, as their expected output says.

One small change: encodeProgram adds a second program.version as SemVer cast (UPLC.ts:1078). Taking the version as a string in encodeTerm and allowsConstrCase removes it.

A follow-up, not for this PR: a var index is still read as an unbounded natural, while Plutus reads it as a Word64 (FlatInstances.hs:363), so (program 1.1.0 (lam (var 2^64))) decodes here and fails in Plutus.

It merges cleanly with #667, so I'll merge it right after that one.

@solidsnakedev
solidsnakedev merged commit 0092feb into IntersectMBO:main Oct 28, 2026
6 checks passed
@solidsnakedev

Copy link
Copy Markdown
Collaborator

Merged, thanks. The new program.version cast is removed in #720.

solidsnakedev added a commit that referenced this pull request Oct 28, 2026
* refactor(uplc): narrow array and Value constants without casts

* refactor(uplc): pass the program version to encodeTerm without a cast

* release: mark the UPLC builtins changeset as minor
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.

UPLC Flat accepts constr/case before 1.1.0 and constr tags of 2^64 or more

2 participants