fix(cbor): encode PlutusData bytes as bounded_bytes via first-class CBOR node#160
Merged
solidsnakedev merged 8 commits intomainfrom Feb 28, 2026
Merged
fix(cbor): encode PlutusData bytes as bounded_bytes via first-class CBOR node#160solidsnakedev merged 8 commits intomainfrom
solidsnakedev merged 8 commits intomainfrom
Conversation
Per the Conway CDDL spec, bounded_bytes = bytes .size (0..64). Byte strings longer than 64 bytes must be encoded as CBOR indefinite-length chunked byte strings (0x5f [chunk]* 0xff). Adds chunkBytesAt option to CodecOptions. Set to 64 in both CML_DATA_DEFAULT_OPTIONS and AIKEN_DEFAULT_OPTIONS. Adds chunkHeaderSize and writeChunkHeader helpers that correctly handle all CBOR byte-string length ranges: <24, <256, <65536, and <2^32. Closes #158
CBOR.BoundedBytes.test.ts: 59 tests covering low-level chunk structure, round-trips (65/128/256/1024 bytes), PlutusData integration, Aiken options, and property-based tests with FastCheck. CBOR.Aiken.test.ts: two new tests mirroring the Aiken spec — encode_bytearray_bounded_65 and encode_bytearray_exactly_64.
Adds encode_bytearray_bounded_65 and encode_bytearray_exactly_64 to cbor_encoding_spec.ak — source-of-truth for chunked encoding behavior. The TS Aiken mirror tests are derived from these.
Replaces the chunkBytesAt-based approach with a dedicated BoundedBytes node that enforces the Conway bounded_bytes rule unconditionally at the data-type layer. Updates CBOR.match to handle BoundedBytes explicitly. Removes unused PreEncoded node type.
bounded_bytes is a PlutusData grammar constraint (Conway CDDL), not a serializer option. Chunking any byte string by codec option was wrong; the BoundedBytes CBOR node now enforces the rule unconditionally.
Contributor
There was a problem hiding this comment.
Pull request overview
Adds a first-class BoundedBytes CBOR node so PlutusData byte strings always comply with Conway bounded_bytes = bytes .size (0..64) by chunking >64 bytes, independent of codec options.
Changes:
- Emit
CBOR.BoundedBytes.make(bytes)for PlutusData byte-array leaves during CBOR conversion. - Extend CBOR encoding + pattern matching to recognize and encode
BoundedBytes. - Add unit/integration tests validating chunk structure, round-trips, and Aiken compatibility.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| packages/evolution/src/Data.ts | Wrap PlutusData byte leaves in CBOR.BoundedBytes to enforce bounded-bytes encoding. |
| packages/evolution/src/CBOR.ts | Introduce BoundedBytes node type + encoder path and add a match branch. |
| packages/evolution/test/CBOR.BoundedBytes.test.ts | Add Vitest + property-based tests for bounded-bytes chunking and CBOR walking helper. |
| packages/evolution/test/CBOR.Aiken.test.ts | Add Aiken-compat expectations for 64 vs 65-byte byte arrays. |
| packages/evolution/test/spec/lib/cbor_encoding_spec.ak | Add Aiken spec assertions for bounded-bytes chunking behavior. |
| packages/evolution-devnet/test/TxBuilder.Scripts.test.ts | Add devnet integration test building a tx with a >64-byte redeemer payload. |
| .changeset/conway-bounded-bytes-fix.md | Document the behavior change and release as a patch. |
Comments suppressed due to low confidence (5)
packages/evolution/src/CBOR.ts:1
- Making
boundedBytesa required branch inCBOR.matchis a source-breaking change for any existing call sites (they must now implement an extra handler). This conflicts with the changeset labeling the release as apatch. Consider either (a) makingboundedBytesoptional with a sensible default (e.g., fall back tobytes), or (b) introducing a new matcher variant and keeping the old signature stable; otherwise the package versioning should be bumped to a breaking release.
import { Data, Either as E, ParseResult, Schema } from "effect"
packages/evolution/test/CBOR.BoundedBytes.test.ts:343
readLengthreturns0for additional info27(8-byte length), which can makeverifyNoBytestringExceedscompute incorrect offsets or loop incorrectly when encountering large definite-length items. It should handleadditional === 27(and ideally throw for31/ unsupported cases) so the walker behaves correctly for all valid CBOR inputs.
const readLength = (data: Uint8Array, offset: number): number => {
const additional = data[offset] & 0x1f
if (additional < 24) return additional
if (additional === 24) return data[offset + 1]
if (additional === 25) return (data[offset + 1] << 8) | data[offset + 2]
if (additional === 26)
return (data[offset + 1] << 24) | (data[offset + 2] << 16) | (data[offset + 3] << 8) | data[offset + 4]
return 0
}
packages/evolution-devnet/test/TxBuilder.Scripts.test.ts:17
- This devnet test now depends on a fixture located under another package’s test/spec directory. That cross-package relative import is brittle in monorepo setups (e.g., running package tests in isolation, different working directories, or packaging constraints). Consider moving the fixture into a shared test-fixtures location intended for cross-package reuse, or inlining/minimizing the required validator data within the devnet test package.
import plutusJson from "../../evolution/test/spec/plutus.json"
packages/evolution/src/CBOR.ts:741
- BoundedBytes detection logic is duplicated here instead of reusing the exported
BoundedBytes.istype guard (added later in the file). To reduce duplication and keep the tag check consistent, consider hoisting a localisBoundedByteshelper aboveinternalEncodeSync(or moving theBoundedBytesdefinition earlier) and using it in both places.
export const internalEncodeSync = (value: CBOR, options: CodecOptions = CML_DEFAULT_OPTIONS): Uint8Array => {
packages/evolution/src/CBOR.ts:759
- BoundedBytes detection logic is duplicated here instead of reusing the exported
BoundedBytes.istype guard (added later in the file). To reduce duplication and keep the tag check consistent, consider hoisting a localisBoundedByteshelper aboveinternalEncodeSync(or moving theBoundedBytesdefinition earlier) and using it in both places.
// BoundedBytes: PlutusData byte strings, encoded per Conway CDDL bounded_bytes = bytes .size (0..64)
if (
typeof value === "object" &&
value !== null &&
"_tag" in value &&
(value as { _tag: unknown })._tag === "BoundedBytes"
) {
return encodeBoundedBytesSync((value as { _tag: "BoundedBytes"; bytes: Uint8Array }).bytes)
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
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.
The Conway CDDL spec defines
bounded_bytes = bytes .size (0..64)as a PlutusData grammar constraint, not a serialiser option. The oldchunkBytesAtfield onCodecOptionsencoded that rule in the wrong place — it would chunk any byte string (hashes, keys, scripts) rather than only PlutusData leaves, and it could be silently omitted by passing different options.Introduces
BoundedBytesas a first-class CBOR node type.plutusDataToCBORValuenow wraps byte-array leaves inBoundedBytes.make(), andencodeBoundedBytesSyncapplies the ≤64-byte chunk rule unconditionally regardless of whichCodecOptionsare passed.chunkBytesAtis removed fromCodecOptionsentirely. Also removes the deadPreEncodedunion variant which had no callers.Closes #158