Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/plutus-data-presets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@evolution-sdk/evolution": minor
---

The new `CBOR.PLUTUS_DATA_OPTIONS` preset encodes Plutus data in the layout the node writes. It writes non-empty lists and constructor fields with indefinite length, maps with definite length, the empty list as `80`, and the empty map as `a0`.

`UPLC.applyParamsToScript` and `UPLC.dataConstant` now encode parameters with `PLUTUS_DATA_OPTIONS` by default, so a map parameter gives the same bytes and script hash as `aiken blueprint apply`. Before, a map parameter became a list of pairs, which is a different value and gives a different script hash. Scripts applied with map parameters therefore get a new hash.

`CBOR.AIKEN_DEFAULT_OPTIONS` now matches Aiken `cbor.serialise()`, which writes `Pairs` as a definite map, and is deprecated in favor of `PLUTUS_DATA_OPTIONS`. The new `CBOR.CML_DATA_DEFINITE_OPTIONS` writes lists and maps with definite length, like CML `PlutusData.to_cbor_hex()`. `CBOR.CARDANO_NODE_DATA_OPTIONS` is deprecated in favor of it, since the node does not write that layout.

The default for `Data` encoding stays `CBOR.CML_DATA_DEFAULT_OPTIONS`, and transaction encoding is unchanged.
8 changes: 5 additions & 3 deletions docs/content/docs/encoding/cbor.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,11 @@ The plain `toCBORHex`/`toCBORBytes` APIs accept a `CodecOptions` argument that c
|--------|----------|
| `CML_DEFAULT_OPTIONS` *(default)* | General Cardano use — definite lengths, minimal integer encoding |
| `CANONICAL_OPTIONS` | RFC 8949 canonical: sorted keys, minimal encoding |
| `CML_DATA_DEFAULT_OPTIONS` | Plutus data with indefinite arrays/maps |
| `AIKEN_DEFAULT_OPTIONS` | Aiken `cbor.serialise()` — indefinite arrays, maps as pairs |
| `CARDANO_NODE_DATA_OPTIONS` | Definite Plutus data (tooling compatibility) |
| `PLUTUS_DATA_OPTIONS` | Plutus data in the node layout: indefinite lists and constructor fields, definite maps. The default for `UPLC.applyParamsToScript` and `UPLC.dataConstant`; matches `cardano-cli`, Aiken `cbor.serialise()`, and `aiken blueprint apply` except for constructor indices above 127 and the integer -2^64 |
| `CML_DATA_DEFAULT_OPTIONS` | Matches CML `PlutusData.to_cardano_node_format()`: indefinite lists and maps. CML also sorts map keys; this preset keeps insertion order. The default for `Data` encoding |
| `CML_DATA_DEFINITE_OPTIONS` | Matches CML `PlutusData.to_cbor_hex()` on new data: definite lists and maps |
| `AIKEN_DEFAULT_OPTIONS` | Deprecated alias of `PLUTUS_DATA_OPTIONS` |
| `CARDANO_NODE_DATA_OPTIONS` | Deprecated alias of `CML_DATA_DEFINITE_OPTIONS` |

```typescript twoslash
import { CBOR } from "@evolution-sdk/evolution"
Expand Down
2 changes: 1 addition & 1 deletion docs/content/docs/encoding/uplc.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ import { UPLC, Data } from "@evolution-sdk/evolution"
// Create a data constant from PlutusData
const term = UPLC.dataConstant(Data.int(42n))

// With custom CBOR options (default is Aiken-compatible)
// The default options match the node and Aiken (PLUTUS_DATA_OPTIONS)
const aikenTerm = UPLC.dataConstant(
Data.constr(0n, [Data.int(1n)]),
)
Expand Down
6 changes: 3 additions & 3 deletions docs/content/docs/introduction/important-defaults.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,13 @@ const tx2 = await template.build() // No contamination from tx1

### CBOR: Aiken-Compatible by Default

`UPLC.applyParamsToScript()` uses Aiken-compatible CBOR encoding (indefinite-length arrays/maps) by default. If you're using a different Plutus toolchain, you may need different options.
`UPLC.applyParamsToScript()` encodes parameters with `CBOR.PLUTUS_DATA_OPTIONS` by default: indefinite-length lists and constructor fields, definite-length maps. This is the layout the node, `cardano-cli`, and `aiken blueprint apply` use. If you're using a different Plutus toolchain, you may need different options.

```typescript
// Default: Aiken encoding
// Default: node layout, same as aiken blueprint apply
UPLC.applyParamsToScript(script, params)

// For CML-compatible encoding:
// Indefinite lists and maps, like CML to_cardano_node_format() (map keys keep insertion order):
UPLC.applyParamsToScript(script, params, CBOR.CML_DATA_DEFAULT_OPTIONS)
```

Expand Down
6 changes: 3 additions & 3 deletions docs/content/docs/smart-contracts/apply-params.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -122,19 +122,19 @@ await signed.submit()

## CBOR Encoding Options

By default, `applyParamsToScript` uses Aiken-compatible encoding (indefinite-length arrays and maps). If your script was compiled with a different tool, you can pass a different CBOR preset:
By default, `applyParamsToScript` uses `CBOR.PLUTUS_DATA_OPTIONS`: indefinite-length lists and constructor fields, definite-length maps, the same bytes as `aiken blueprint apply`. If your script was compiled with a different tool, you can pass a different CBOR preset:

```typescript twoslash
import { CBOR, Data, UPLC } from "@evolution-sdk/evolution"

declare const compiledScript: string

// With default Aiken encoding (indefinite-length arrays/maps)
// With the default node layout (same as aiken blueprint apply)
const applied = UPLC.applyParamsToScript(compiledScript, [
Data.int(42n),
])

// With CML-compatible encoding (definite-length)
// With indefinite lists and maps, like CML to_cardano_node_format() (map keys keep insertion order)
const appliedCml = UPLC.applyParamsToScript(
compiledScript,
[Data.int(42n)],
Expand Down
108 changes: 78 additions & 30 deletions packages/evolution/src/CBOR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,13 @@ export type CodecOptions =
| {
readonly mode: "canonical"
readonly mapsAsObjects?: boolean
/**
* Encode every map as an array of `[key, value]` pairs.
*
* @deprecated This turns a map into a list, which is a different value
* with a different hash. No Plutus data encoder writes maps this way, and
* no option replaces it: leave it unset so maps stay maps.
*/
readonly encodeMapAsPairs?: boolean
}
| {
Expand All @@ -178,6 +185,13 @@ export type CodecOptions =
readonly sortMapKeys: boolean
readonly useMinimalEncoding: boolean
readonly mapsAsObjects?: boolean
/**
* Encode every map as an array of `[key, value]` pairs.
*
* @deprecated This turns a map into a list, which is a different value
* with a different hash. No Plutus data encoder writes maps this way, and
* no option replaces it: leave it unset so maps stay maps.
*/
readonly encodeMapAsPairs?: boolean
}

Expand Down Expand Up @@ -208,51 +222,74 @@ export const CML_DEFAULT_OPTIONS: CodecOptions = {
} as const

/**
* Default CBOR encoding options for PlutusData.
* CBOR encoding options for PlutusData in the layout the Cardano node writes.
*
* Uses indefinite-length arrays and maps. The `bounded_bytes` constraint
* (Conway CDDL: byte strings ≤ 64 bytes) is enforced at the data-type layer
* via the `BoundedBytes` CBOR node, independent of these codec options.
* - Non-empty lists and constructor fields: indefinite-length (`9f...ff`)
* - Maps: definite-length
* - Empty list: `80`; empty map: `a0`
*
* @since 1.0.0
* This matches `encodeData` in the Haskell `PlutusCore.Data` module,
* `cardano-cli hash-script-data`, Aiken `cbor.serialise()`, and
* `aiken blueprint apply`. Two cases still differ from the node: a
* constructor index above 127 (tag 102) writes its `[index, fields]` pair
* indefinite where the node writes `82`, and the integer -2^64 is written as a
* negative bignum where the node writes `3bffffffffffffffff`. The
* `bounded_bytes` constraint (Conway CDDL: byte strings of at most 64 bytes)
* is enforced at the data-type layer via the `BoundedBytes` CBOR node,
* independent of these codec options.
*
* @since 2.0.0
* @category constants
*/
export const CML_DATA_DEFAULT_OPTIONS: CodecOptions = {
export const PLUTUS_DATA_OPTIONS: CodecOptions = {
mode: "custom",
useIndefiniteArrays: true,
useIndefiniteMaps: true,
useIndefiniteMaps: false,
useDefiniteForEmpty: true,
sortMapKeys: false,
useMinimalEncoding: true,
mapsAsObjects: false
} as const

/**
* Aiken-compatible CBOR encoding options.
*
* Matches the encoding produced by `cbor.serialise()` in Aiken:
* - Indefinite-length arrays (`9f...ff`)
* - Maps encoded as arrays of pairs (not CBOR maps)
* - Strings as byte arrays (major type 2, not 3)
* - Constructor tags: 121–127 for indices 0–6, then 1280+ for 7+
* CBOR encoding options for PlutusData matching CML
* `PlutusData.to_cardano_node_format()`, except that CML also sorts map keys
* and this preset keeps insertion order. This is the current default for
* `Data` encoding.
*
* PlutusData byte strings are chunked per the Conway `bounded_bytes` rule
* via the `BoundedBytes` CBOR node, independent of these codec options.
* Uses indefinite-length lists, constructor fields, and maps. It differs from
* the node layout ({@link PLUTUS_DATA_OPTIONS}) by writing non-empty maps
* indefinite. The `bounded_bytes` constraint (Conway CDDL: byte strings of at
* most 64 bytes) is enforced at the data-type layer via the `BoundedBytes`
* CBOR node, independent of these codec options.
*
* @since 2.0.0
* @since 1.0.0
* @category constants
*/
export const AIKEN_DEFAULT_OPTIONS: CodecOptions = {
export const CML_DATA_DEFAULT_OPTIONS: CodecOptions = {
mode: "custom",
useIndefiniteArrays: true,
useIndefiniteMaps: true,
useDefiniteForEmpty: false,
useDefiniteForEmpty: true,
sortMapKeys: false,
useMinimalEncoding: true,
mapsAsObjects: false,
encodeMapAsPairs: true
mapsAsObjects: false
} as const

/**
* Aiken `cbor.serialise()` encoding options. Same object as
* {@link PLUTUS_DATA_OPTIONS}.
*
* Aiken writes data in the node layout: non-empty lists and constructor
* fields indefinite, `Pairs` as definite maps. Tuples are lists, so they
* encode as indefinite arrays.
*
* @deprecated Use {@link PLUTUS_DATA_OPTIONS}.
* @since 2.0.0
* @category constants
*/
export const AIKEN_DEFAULT_OPTIONS: CodecOptions = PLUTUS_DATA_OPTIONS

/**
* CBOR encoding options that return objects instead of Maps for Schema.Struct compatibility
*
Expand All @@ -270,19 +307,17 @@ export const STRUCT_FRIENDLY_OPTIONS: CodecOptions = {
} as const

/**
* Cardano Node compatible CBOR encoding options for PlutusData
*
* Uses definite-length encoding for arrays and maps, matching the format
* produced by CML's `to_cardano_node_format().to_cbor_hex()`.
* CBOR encoding options for PlutusData matching CML `PlutusData.to_cbor_hex()`
* on freshly built data.
*
* Note: The on-chain format uses indefinite-length (AIKEN_DEFAULT_OPTIONS),
* but this option is useful for testing compatibility with tools that
* expect definite-length encoding.
* Uses definite-length lists, constructor fields, and maps. It differs from
* the node layout ({@link PLUTUS_DATA_OPTIONS}) by writing lists and
* constructor fields definite.
*
* @since 2.0.0
* @category constants
*/
export const CARDANO_NODE_DATA_OPTIONS: CodecOptions = {
export const CML_DATA_DEFINITE_OPTIONS: CodecOptions = {
mode: "custom",
useIndefiniteArrays: false,
useIndefiniteMaps: false,
Expand All @@ -292,6 +327,19 @@ export const CARDANO_NODE_DATA_OPTIONS: CodecOptions = {
mapsAsObjects: false
} as const

/**
* Definite-length PlutusData encoding options. Same object as
* {@link CML_DATA_DEFINITE_OPTIONS}.
*
* The name is wrong: the Cardano node writes non-empty lists and constructor
* fields indefinite. For the node layout use {@link PLUTUS_DATA_OPTIONS}.
*
* @deprecated Use {@link CML_DATA_DEFINITE_OPTIONS}.
* @since 2.0.0
* @category constants
*/
export const CARDANO_NODE_DATA_OPTIONS: CodecOptions = CML_DATA_DEFINITE_OPTIONS

const DEFAULT_OPTIONS: CodecOptions = {
mode: "custom",
useIndefiniteArrays: false,
Expand Down Expand Up @@ -1362,7 +1410,7 @@ const encodeMapEntriesSync = (pairs: Array<[CBOR, CBOR]>, options: CodecOptions,
const encodeAsPairs =
!mapFmt && (options.mode === "canonical" || options.mode === "custom") && options.encodeMapAsPairs === true

// If encoding as array of pairs (Aiken/Plutus style), delegate to array encoding
// If encoding as array of pairs (deprecated encodeMapAsPairs), delegate to array encoding
if (encodeAsPairs) {
const pairArrays = pairs.map(([k, v]) => [k, v] as CBOR)
return encodeArraySync(pairArrays, options)
Expand Down
18 changes: 11 additions & 7 deletions packages/evolution/src/UPLC.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1507,14 +1507,15 @@ const encodeFlatToDoubleCbor = (bytes: Uint8Array): string => {
* Create a UPLC Constant term from PlutusData.
* The data is CBOR-encoded and stored as a constant of type Data.
*
* Uses Aiken-compatible encoding (indefinite-length arrays/maps) by default,
* which matches the on-chain format. An optional `options` parameter allows
* customizing the CBOR encoding for testing or compatibility purposes.
* Encodes with PLUTUS_DATA_OPTIONS by default: the node layout, with
* indefinite-length lists and constructor fields and definite-length maps.
* This matches Aiken `cbor.serialise()`. An optional `options` parameter
* allows customizing the CBOR encoding for testing or compatibility purposes.
*
* @since 2.0.0
* @category constructors
*/
export const dataConstant = (data: Data.Data, options: CBOR.CodecOptions = CBOR.AIKEN_DEFAULT_OPTIONS): Term => ({
export const dataConstant = (data: Data.Data, options: CBOR.CodecOptions = CBOR.PLUTUS_DATA_OPTIONS): Term => ({
type: "Constant",
valueType: "Data",
value: Data.toCBORBytes(data, options)
Expand All @@ -1527,16 +1528,19 @@ export const dataConstant = (data: Data.Data, options: CBOR.CodecOptions = CBOR.
* the script body in a series of Application nodes, where each parameter is
* converted to a UPLC Constant of type Data.
*
* Uses Aiken-compatible encoding (indefinite-length arrays/maps) by default.
* Pass custom CBOR options for compatibility with other encoding formats.
* Encodes parameters with PLUTUS_DATA_OPTIONS by default: the node layout,
* with indefinite-length lists and constructor fields and definite-length
* maps. This matches `aiken blueprint apply`, so the applied script hash
* agrees. Pass custom CBOR options for compatibility with other encoding
* formats.
*
* @since 2.0.0
* @category script
*/
export const applyParamsToScript = (
plutusScript: string,
params: ReadonlyArray<Data.Data>,
options: CBOR.CodecOptions = CBOR.AIKEN_DEFAULT_OPTIONS
options: CBOR.CodecOptions = CBOR.PLUTUS_DATA_OPTIONS
): string => {
// Decode the script to a UPLC Program
const flatBytes = decodeDoubleCborHexToFlat(plutusScript)
Expand Down
64 changes: 36 additions & 28 deletions packages/evolution/test/CBOR.Aiken.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,41 +112,52 @@ describe("Aiken CBOR Encoding Compatibility", () => {
expect(encoded).toBe("9f9f0102ff9f0304ffff")
})

// Test #16: encode_map_empty
it("encode_map_empty: should encode empty map", () => {
const value = Data.map([])
// Test #16: encode_tuples_empty
it("encode_tuples_empty: should encode empty list of tuples", () => {
const value = Data.list([])
const encoded = Data.toCBORHex(value, CBOR.AIKEN_DEFAULT_OPTIONS)
expect(encoded).toBe("80")
})

// Test #17: encode_map_single_entry
it("encode_map_single_entry: should encode single entry map", () => {
const value = Data.map([[1n, Bytes.fromHex("ff")]])
// Test #17: encode_tuples_single_entry
it("encode_tuples_single_entry: should encode [(1, #ff)] as a list of lists", () => {
const value = Data.list([Data.list([1n, Bytes.fromHex("ff")])])
const encoded = Data.toCBORHex(value, CBOR.AIKEN_DEFAULT_OPTIONS)
expect(encoded).toBe("9f9f0141ffffff")
})

// Test #18: encode_map_multiple_entries
it("encode_map_multiple_entries: should encode map with multiple entries", () => {
const value = Data.map([
[Bytes.fromHex("01"), 1n],
[Bytes.fromHex("02"), 2n],
[Bytes.fromHex("03"), 3n]
// Test #18: encode_tuples_multiple_entries
it("encode_tuples_multiple_entries: should encode list of tuples with multiple entries", () => {
const value = Data.list([
Data.list([Bytes.fromHex("01"), 1n]),
Data.list([Bytes.fromHex("02"), 2n]),
Data.list([Bytes.fromHex("03"), 3n])
])
const encoded = Data.toCBORHex(value, CBOR.AIKEN_DEFAULT_OPTIONS)
expect(encoded).toBe("9f9f410101ff9f410202ff9f410303ffff")
})

// Test #19: encode_map_int_keys
it("encode_map_int_keys: should encode map with int keys", () => {
const value = Data.map([
[1n, 100n],
[2n, 200n]
])
// Test #19: encode_tuples_int_keys
it("encode_tuples_int_keys: should encode list of tuples with int keys", () => {
const value = Data.list([Data.list([1n, 100n]), Data.list([2n, 200n])])
const encoded = Data.toCBORHex(value, CBOR.AIKEN_DEFAULT_OPTIONS)
expect(encoded).toBe("9f9f011864ff9f0218c8ffff")
})

// encode_pairs_single: Aiken Pairs encode as a definite-length CBOR map
it("encode_pairs_single: should encode [Pair(1, #ff)] as a definite map", () => {
const value = Data.map([[1n, Bytes.fromHex("ff")]])
const encoded = Data.toCBORHex(value, CBOR.AIKEN_DEFAULT_OPTIONS)
expect(encoded).toBe("a10141ff")
})

// encode_pairs_empty
it("encode_pairs_empty: should encode empty Pairs as an empty map", () => {
const value = Data.map([])
const encoded = Data.toCBORHex(value, CBOR.AIKEN_DEFAULT_OPTIONS)
expect(encoded).toBe("a0")
})

// Test #20: encode_option_some
it("encode_option_some: should encode Some(42)", () => {
const OptionInt = TSchema.UndefinedOr(TSchema.Integer)
Expand Down Expand Up @@ -326,23 +337,20 @@ describe("Aiken CBOR Encoding Compatibility", () => {
expect(encoded).toBe("d8799f9f010203ffff")
})

// Test #35: encode_map_with_option_values
it("encode_map_with_option_values: should encode map with option values", () => {
// Test #35: encode_tuples_with_option_values
it("encode_tuples_with_option_values: should encode list of tuples with option values", () => {
const OptionInt = TSchema.UndefinedOr(TSchema.Integer)
const some100 = Data.withSchema(OptionInt).toData(100n)
const none = Data.withSchema(OptionInt).toData(undefined)
const value = Data.map([
[1n, some100],
[2n, none]
])
const value = Data.list([Data.list([1n, some100]), Data.list([2n, none])])
const encoded = Data.toCBORHex(value, CBOR.AIKEN_DEFAULT_OPTIONS)
expect(encoded).toBe("9f9f01d8799f1864ffff9f02d87a80ffff")
})

// Test #36: encode_map_nested_as_value
it("encode_map_nested_as_value: should encode map with nested map value", () => {
const innerMap = Data.map([[2n, 3n]])
const value = Data.map([[1n, innerMap]])
// Test #36: encode_tuples_nested_as_value
it("encode_tuples_nested_as_value: should encode list of tuples with a nested list of tuples", () => {
const inner = Data.list([Data.list([2n, 3n])])
const value = Data.list([Data.list([1n, inner])])
const encoded = Data.toCBORHex(value, CBOR.AIKEN_DEFAULT_OPTIONS)
expect(encoded).toBe("9f9f019f9f0203ffffffff")
})
Expand Down
Loading
Loading