Skip to content

fix(cbor): encode PlutusData bytes as bounded_bytes via first-class CBOR node#160

Merged
solidsnakedev merged 8 commits intomainfrom
fix/conway-bounded-bytes
Feb 28, 2026
Merged

fix(cbor): encode PlutusData bytes as bounded_bytes via first-class CBOR node#160
solidsnakedev merged 8 commits intomainfrom
fix/conway-bounded-bytes

Conversation

@solidsnakedev
Copy link
Copy Markdown
Collaborator

@solidsnakedev solidsnakedev commented Feb 28, 2026

The Conway CDDL spec defines bounded_bytes = bytes .size (0..64) as a PlutusData grammar constraint, not a serialiser option. The old chunkBytesAt field on CodecOptions encoded 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 BoundedBytes as a first-class CBOR node type. plutusDataToCBORValue now wraps byte-array leaves in BoundedBytes.make(), and encodeBoundedBytesSync applies the ≤64-byte chunk rule unconditionally regardless of which CodecOptions are passed. chunkBytesAt is removed from CodecOptions entirely. Also removes the dead PreEncoded union variant which had no callers.

Closes #158

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.
Copilot AI review requested due to automatic review settings February 28, 2026 01:43
@solidsnakedev solidsnakedev added the bug Something isn't working label Feb 28, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 boundedBytes a required branch in CBOR.match is 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 a patch. Consider either (a) making boundedBytes optional with a sensible default (e.g., fall back to bytes), 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

  • readLength returns 0 for additional info 27 (8-byte length), which can make verifyNoBytestringExceeds compute incorrect offsets or loop incorrectly when encountering large definite-length items. It should handle additional === 27 (and ideally throw for 31 / 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.is type guard (added later in the file). To reduce duplication and keep the tag check consistent, consider hoisting a local isBoundedBytes helper above internalEncodeSync (or moving the BoundedBytes definition 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.is type guard (added later in the file). To reduce duplication and keep the tag check consistent, consider hoisting a local isBoundedBytes helper above internalEncodeSync (or moving the BoundedBytes definition 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.

@solidsnakedev solidsnakedev merged commit 9c16997 into main Feb 28, 2026
9 of 11 checks passed
@solidsnakedev solidsnakedev deleted the fix/conway-bounded-bytes branch February 28, 2026 02:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: CBOR encoder violates Conway bounded_bytes constraint for byte strings > 64 bytes

2 participants