From b6c2538bb99db215482b37e4bed50663376b5336 Mon Sep 17 00:00:00 2001 From: solidsnakedev Date: Fri, 9 Oct 2026 09:47:35 -0600 Subject: [PATCH 1/4] feat(data): default Plutus data to PLUTUS_DATA_OPTIONS --- packages/evolution/src/CBOR.ts | 32 +++++++++++++++++--------------- packages/evolution/src/Data.ts | 21 +++++++++++---------- 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/packages/evolution/src/CBOR.ts b/packages/evolution/src/CBOR.ts index 0fcac8ed..1221e492 100644 --- a/packages/evolution/src/CBOR.ts +++ b/packages/evolution/src/CBOR.ts @@ -228,7 +228,8 @@ export const CML_DEFAULT_OPTIONS: CodecOptions = { * - Maps: definite-length * - Empty list: `80`; empty map: `a0` * - * `Data.toCBORBytes` with these options writes the bytes the node writes. + * The `Data` default. `Data.toCBORBytes` with these options writes the bytes + * the node writes. * The rules that do not depend on options are applied by the Plutus data * encoder under every preset: byte strings over 64 bytes in 64-byte chunks, * bignums only outside -2^64 to 2^64 - 1 with their bytes chunked the same @@ -248,17 +249,19 @@ export const PLUTUS_DATA_OPTIONS: CodecOptions = { } as const /** - * 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. + * CBOR encoding options for PlutusData with non-empty lists, constructor + * fields and maps indefinite, map keys in the order given. This was the + * `Data` default before {@link PLUTUS_DATA_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 applied by the Plutus data encoder, independent of these - * codec options. + * It matches no tool exactly. The node, cardano-cli and Aiken write maps + * definite. CML 6.2.0 `PlutusData.to_cardano_node_format()` writes maps + * indefinite but also sorts map keys. For a constructor index above 127 the + * Plutus data encoder writes the tag 102 `[index, fields]` pair definite + * under every preset, including this one. Use it only to reproduce the bytes + * and datum hashes the SDK wrote before {@link PLUTUS_DATA_OPTIONS} became + * the default. * + * @deprecated Use {@link PLUTUS_DATA_OPTIONS}. * @since 1.0.0 * @category constants */ @@ -357,8 +360,7 @@ export type TxCodecOptions = { * * - `ledger` is {@link CML_DEFAULT_OPTIONS}, CML's transaction encoding: map * keys in insertion order, definite lengths. - * - `plutusData` is {@link CML_DATA_DEFAULT_OPTIONS}, the current `Data` - * default. + * - `plutusData` is {@link PLUTUS_DATA_OPTIONS}, the `Data` default. * * It does not match cardano-cli, which sorts map keys and writes indefinite * lengths above 23 entries. @@ -368,7 +370,7 @@ export type TxCodecOptions = { */ export const TX_DEFAULT_OPTIONS: TxCodecOptions = { ledger: CML_DEFAULT_OPTIONS, - plutusData: CML_DATA_DEFAULT_OPTIONS + plutusData: PLUTUS_DATA_OPTIONS } /** @@ -386,7 +388,7 @@ export const TX_CANONICAL_OPTIONS: TxCodecOptions = { /** * Turns plain options into transaction options: `ledger` is the options and * `plutusData` is the options too, except that {@link CML_DEFAULT_OPTIONS} - * gives {@link CML_DATA_DEFAULT_OPTIONS}, as {@link TX_DEFAULT_OPTIONS} does. + * gives {@link PLUTUS_DATA_OPTIONS}, as {@link TX_DEFAULT_OPTIONS} does. * Transaction options are returned as they are. * * @since 2.0.0 @@ -395,7 +397,7 @@ export const TX_CANONICAL_OPTIONS: TxCodecOptions = { export const toTxCodecOptions = (options: TxCodecOptions | CodecOptions): TxCodecOptions => "ledger" in options ? options - : { ledger: options, plutusData: options === CML_DEFAULT_OPTIONS ? CML_DATA_DEFAULT_OPTIONS : options } + : { ledger: options, plutusData: options === CML_DEFAULT_OPTIONS ? PLUTUS_DATA_OPTIONS : options } const DEFAULT_OPTIONS: CodecOptions = { mode: "custom", diff --git a/packages/evolution/src/Data.ts b/packages/evolution/src/Data.ts index 091bbe14..4d93ef83 100644 --- a/packages/evolution/src/Data.ts +++ b/packages/evolution/src/Data.ts @@ -490,12 +490,13 @@ export const arbitrary = arbitraryPlutusData(3) // ============================================================================ /** - * Default CBOR options for Data encoding/decoding + * Default CBOR options for Data encoding/decoding: {@link CBOR.PLUTUS_DATA_OPTIONS}, + * the layout the node writes. * * @since 2.0.0 * @category constants */ -export const DEFAULT_CBOR_OPTIONS = CBOR.CML_DATA_DEFAULT_OPTIONS +export const DEFAULT_CBOR_OPTIONS = CBOR.PLUTUS_DATA_OPTIONS /** * Convert a big-endian byte array to a positive bigint @@ -1041,7 +1042,7 @@ export const FromCDDL = Schema.transformOrFail(CDDLSchema, Schema.typeSchema(Dat * @since 2.0.0 * @category schemas */ -export const FromCBORBytes = (options: CBOR.CodecOptions = CBOR.CML_DATA_DEFAULT_OPTIONS) => { +export const FromCBORBytes = (options: CBOR.CodecOptions = DEFAULT_CBOR_OPTIONS) => { const fromBytes = Schema.compose(CBOR.FromBytes(options), FromCDDL) const layout = toLayout(options) return Schema.transformOrFail(Schema.Uint8ArrayFromSelf, Schema.typeSchema(DataSchema), { @@ -1071,7 +1072,7 @@ export const FromCBORBytes = (options: CBOR.CodecOptions = CBOR.CML_DATA_DEFAULT * @since 2.0.0 * @category schemas */ -export const FromCBORHex = (options: CBOR.CodecOptions = CBOR.CML_DATA_DEFAULT_OPTIONS) => +export const FromCBORHex = (options: CBOR.CodecOptions = DEFAULT_CBOR_OPTIONS) => Schema.compose( Schema.Uint8ArrayFromHex, // string → Uint8Array FromCBORBytes(options) // Uint8Array → Data @@ -1096,7 +1097,7 @@ export const FromCBORHex = (options: CBOR.CodecOptions = CBOR.CML_DATA_DEFAULT_O * @since 2.0.0 * @category transformation */ -export const toCBORBytes = (data: Data, options: CBOR.CodecOptions = CBOR.CML_DATA_DEFAULT_OPTIONS): Uint8Array => +export const toCBORBytes = (data: Data, options: CBOR.CodecOptions = DEFAULT_CBOR_OPTIONS): Uint8Array => encodeData(data, toLayout(options)) /** @@ -1105,7 +1106,7 @@ export const toCBORBytes = (data: Data, options: CBOR.CodecOptions = CBOR.CML_DA * @since 2.0.0 * @category transformation */ -export const toCBORHex = (data: Data, options: CBOR.CodecOptions = CBOR.CML_DATA_DEFAULT_OPTIONS): string => +export const toCBORHex = (data: Data, options: CBOR.CodecOptions = DEFAULT_CBOR_OPTIONS): string => Bytes.toHex(toCBORBytes(data, options)) /** @@ -1114,7 +1115,7 @@ export const toCBORHex = (data: Data, options: CBOR.CodecOptions = CBOR.CML_DATA * @since 2.0.0 * @category transformation */ -export const fromCBORBytes = (bytes: Uint8Array, options: CBOR.CodecOptions = CBOR.CML_DATA_DEFAULT_OPTIONS): Data => +export const fromCBORBytes = (bytes: Uint8Array, options: CBOR.CodecOptions = DEFAULT_CBOR_OPTIONS): Data => Schema.decodeSync(FromCBORBytes(options))(bytes) /** @@ -1123,7 +1124,7 @@ export const fromCBORBytes = (bytes: Uint8Array, options: CBOR.CodecOptions = CB * @since 2.0.0 * @category transformation */ -export const fromCBORHex = (hex: string, options: CBOR.CodecOptions = CBOR.CML_DATA_DEFAULT_OPTIONS): Data => +export const fromCBORHex = (hex: string, options: CBOR.CodecOptions = DEFAULT_CBOR_OPTIONS): Data => Schema.decodeSync(FromCBORHex(options))(hex) /** @@ -1148,7 +1149,7 @@ export const withSchema = ( /** * Compute the hash of PlutusData using blake2b-256 over its CBOR encoding. - * Defaults to CML_DATA_DEFAULT_OPTIONS (indefinite-length arrays/maps). + * Defaults to PLUTUS_DATA_OPTIONS, the node layout. * * @since 2.0.0 * @category hashing @@ -1171,7 +1172,7 @@ export const withSchema = ( */ export const toDatumHash = ( data: Data, - options: CBOR.CodecOptions = CBOR.CML_DATA_DEFAULT_OPTIONS + options: CBOR.CodecOptions = DEFAULT_CBOR_OPTIONS ): DatumHash.DatumHash => { const bytes = toCBORBytes(data, options) const digest = blake2b(bytes, { dkLen: 32 }) From 3348c3e02f0204ce40cc1491ff6732da27d66bf2 Mon Sep 17 00:00:00 2001 From: solidsnakedev Date: Fri, 9 Oct 2026 09:47:35 -0600 Subject: [PATCH 2/4] test: cover the Plutus data default layout --- .../test/Transaction.InlineDatumBytes.test.ts | 19 +++++---- .../Transaction.RedeemerMapBytesKey.test.ts | 10 ++--- .../test/TxBuilder.WitnessDatum.test.ts | 22 +++++----- .../test/CBOR.PlutusDataPresets.test.ts | 14 ++++++- packages/evolution/test/Data.golden.test.ts | 41 +++++++++++-------- packages/evolution/test/Data.test.ts | 2 +- .../test/DatumOption.OriginalBytes.test.ts | 4 +- packages/evolution/test/TSchema.test.ts | 2 +- 8 files changed, 65 insertions(+), 49 deletions(-) diff --git a/packages/evolution-devnet/test/Transaction.InlineDatumBytes.test.ts b/packages/evolution-devnet/test/Transaction.InlineDatumBytes.test.ts index a8841784..8bcc7060 100644 --- a/packages/evolution-devnet/test/Transaction.InlineDatumBytes.test.ts +++ b/packages/evolution-devnet/test/Transaction.InlineDatumBytes.test.ts @@ -84,10 +84,11 @@ describe("Inline datum bytes (Devnet Submit)", () => { const client = createTestClient() const myAddress = await client.address() - // The builder writes both maps indefinite (bf...ff). The second one is then - // rewritten as a definite map (a1...), one byte shorter, so the fee still covers it. - const indefiniteDatum = "bf0102ff" - const definiteDatum = "a10304" + // The builder writes both maps definite (a1...). The second one, {3: 1000}, is + // then rewritten as the indefinite map {3: 4} (bf...ff), one byte shorter, so + // the fee still covers it. + const definiteDatum = "a10102" + const indefiniteDatum = "bf0304ff" const signBuilder = await client .newTx() .payToAddress({ @@ -98,14 +99,14 @@ describe("Inline datum bytes (Devnet Submit)", () => { .payToAddress({ address: myAddress, assets: Cardano.Assets.fromLovelace(5_000_000n), - datum: new InlineDatum.InlineDatum({ data: PlutusData.map([[PlutusData.int(3n), PlutusData.int(4n)]]) }) + datum: new InlineDatum.InlineDatum({ data: PlutusData.map([[PlutusData.int(3n), PlutusData.int(1000n)]]) }) }) .build({ availableUtxos: [...genesisUtxos] }) const builtHex = Transaction.toCBORHex(await signBuilder.toTransaction()) - expect(builtHex.split("d81844" + indefiniteDatum).length).toBe(2) - expect(builtHex.split("d81844bf0304ff").length).toBe(2) - const txHex = builtHex.replace("d81844bf0304ff", "d81843" + definiteDatum) + expect(builtHex.split("d81843" + definiteDatum).length).toBe(2) + expect(builtHex.split("d81845a1031903e8").length).toBe(2) + const txHex = builtHex.replace("d81845a1031903e8", "d81844" + indefiniteDatum) const txBytes = Bytes.fromHex(txHex) const bodyBytes = Transaction.extractBodyBytes(txBytes) const expectedTxId = TransactionHash.toHex(TransactionBody.toHashFromBytes(bodyBytes)) @@ -131,6 +132,6 @@ describe("Inline datum bytes (Devnet Submit)", () => { const datums = (utxos.result as Array<{ index: number; datum?: string }>) .sort((a, b) => a.index - b.index) .map((utxo) => utxo.datum) - expect(datums).toEqual([indefiniteDatum, definiteDatum]) + expect(datums).toEqual([definiteDatum, indefiniteDatum]) }) }) diff --git a/packages/evolution-devnet/test/Transaction.RedeemerMapBytesKey.test.ts b/packages/evolution-devnet/test/Transaction.RedeemerMapBytesKey.test.ts index 5eb90f9e..c706dec5 100644 --- a/packages/evolution-devnet/test/Transaction.RedeemerMapBytesKey.test.ts +++ b/packages/evolution-devnet/test/Transaction.RedeemerMapBytesKey.test.ts @@ -129,11 +129,11 @@ describe("Redeemer map with byte-string key (Devnet Submit)", () => { const scriptUtxos = await client.getUtxos(scriptAddress) expect(scriptUtxos.length).toBe(1) - // The builder writes {h'01': [100, 2]} in the data default, an indefinite map and - // list (bf 41 01 9f 1864 02 ff ff). It is rewritten as {h'01': [1, 2]} with a - // definite map and an indefinite list (a1 41 01 9f 01 02 ff), which is shorter, - // so the fee still covers it. The script ignores the redeemer. - const builtRedeemerData = "bf41019f186402ffff" + // The builder writes {h'01': [100, 2]} in the data default, a definite map and + // an indefinite list (a1 41 01 9f 1864 02 ff). It is rewritten as {h'01': [1, 2]} + // in the same layout (a1 41 01 9f 01 02 ff), which is shorter, so the fee still + // covers it. The script ignores the redeemer. + const builtRedeemerData = "a141019f186402ff" const redeemerData = "a141019f0102ff" const signBuilder = await client .newTx() diff --git a/packages/evolution-devnet/test/TxBuilder.WitnessDatum.test.ts b/packages/evolution-devnet/test/TxBuilder.WitnessDatum.test.ts index 9311eeae..38ce9d6a 100644 --- a/packages/evolution-devnet/test/TxBuilder.WitnessDatum.test.ts +++ b/packages/evolution-devnet/test/TxBuilder.WitnessDatum.test.ts @@ -11,7 +11,7 @@ * signs with Transaction.addVKeyWitnessesBytes, and submits. Each case uses * one set of options for the script data hash and the encoding, and their * plutusData options for the datum hash: no options, CBOR.CML_DEFAULT_OPTIONS, - * CBOR.TX_CANONICAL_OPTIONS, or plutusData set to PLUTUS_DATA_OPTIONS. The + * CBOR.TX_CANONICAL_OPTIONS, or plutusData set to CML_DATA_DEFAULT_OPTIONS. The * node accepts the spend only when the written datum hashes to the locked * datum hash and the script data hash covers the datum and redeemer as * written. @@ -57,8 +57,8 @@ describe("Witness datum layout (Devnet Submit)", () => { const script = new PlutusV3.PlutusV3({ bytes: Bytes.fromHex(alwaysSucceed.compiledCode) }) const scriptAddress = Address.Address.make({ networkId: 0, paymentCredential: ScriptHash.fromScript(script) }) - // Constr 0 [{1: 2}, [3]]: the data default writes its map and lists - // indefinite, PLUTUS_DATA_OPTIONS its map definite + // Constr 0 [{1: 2}, [3]]: the data default writes its lists indefinite and + // its map definite, CML_DATA_DEFAULT_OPTIONS its map indefinite const mapAndList = Data.constr(0n, [Data.map([[Data.int(1n), Data.int(2n)]]), Data.list([Data.int(3n)])]) // Each datum, the options it is written with, and the bytes they give for @@ -75,15 +75,15 @@ describe("Witness datum layout (Devnet Submit)", () => { name: "Constr 0 [{1: 2}, [3]] with no options", datum: mapAndList, options: undefined, - datumHex: "d8799fbf0102ff9f03ffff", - redeemerHex: "d8799fbf0102ff9f03ffff" + datumHex: "d8799fa101029f03ffff", + redeemerHex: "d8799fa101029f03ffff" }, { name: "Constr 0 [1] with CBOR.CML_DEFAULT_OPTIONS", datum: Data.constr(0n, [Data.int(1n)]), options: CBOR.CML_DEFAULT_OPTIONS, datumHex: "d8799f01ff", - redeemerHex: "d8799fbf0102ff9f03ffff" + redeemerHex: "d8799fa101029f03ffff" }, { name: "Constr 0 [{1: 2}, [3]] with CBOR.TX_CANONICAL_OPTIONS", @@ -93,11 +93,11 @@ describe("Witness datum layout (Devnet Submit)", () => { redeemerHex: "d87982a101028103" }, { - name: "Constr 0 [{1: 2}, [3]] with plutusData PLUTUS_DATA_OPTIONS", + name: "Constr 0 [{1: 2}, [3]] with plutusData CML_DATA_DEFAULT_OPTIONS", datum: mapAndList, - options: { ledger: CBOR.CML_DEFAULT_OPTIONS, plutusData: CBOR.PLUTUS_DATA_OPTIONS }, - datumHex: "d8799fa101029f03ffff", - redeemerHex: "d8799fa101029f03ffff" + options: { ledger: CBOR.CML_DEFAULT_OPTIONS, plutusData: CBOR.CML_DATA_DEFAULT_OPTIONS }, + datumHex: "d8799fbf0102ff9f03ffff", + redeemerHex: "d8799fbf0102ff9f03ffff" } ] @@ -283,7 +283,7 @@ describe("Witness datum layout (Devnet Submit)", () => { .build() const builtHex = Transaction.toCBORHex(await signBuilder.toTransaction()) expect(builtHex).toContain(`05a182000082${Data.toCBORHex(mapAndList)}`) - expect(Data.toCBORHex(mapAndList)).toBe("d8799fbf0102ff9f03ffff") + expect(Data.toCBORHex(mapAndList)).toBe("d8799fa101029f03ffff") const txHash = await (await signBuilder.sign()).submit() expect(await client.awaitTx(txHash, 1000)).toBe(true) diff --git a/packages/evolution/test/CBOR.PlutusDataPresets.test.ts b/packages/evolution/test/CBOR.PlutusDataPresets.test.ts index 85e0e90d..2be602f0 100644 --- a/packages/evolution/test/CBOR.PlutusDataPresets.test.ts +++ b/packages/evolution/test/CBOR.PlutusDataPresets.test.ts @@ -59,6 +59,11 @@ describe("PLUTUS_DATA_OPTIONS matches the node encoding", () => { const encoded = Data.toCBORBytes(data, CBOR.PLUTUS_DATA_OPTIONS) expect(Bytes.toHex(blake2b(encoded, { dkLen: 32 }))).toBe(hash) }) + + it(`${name}: the Data default writes the same bytes and datum hash`, () => { + expect(Data.toCBORHex(data)).toBe(Data.toCBORHex(data, CBOR.PLUTUS_DATA_OPTIONS)) + expect(Bytes.toHex(Data.toDatumHash(data).hash)).toBe(hash) + }) } it("encodes the empty list as 80 and the empty map as a0", () => { @@ -71,8 +76,13 @@ describe("PLUTUS_DATA_OPTIONS matches the node encoding", () => { expect(CBOR.CARDANO_NODE_DATA_OPTIONS).toBe(CBOR.CML_DATA_DEFINITE_OPTIONS) }) - it("leaves the Data default on CML_DATA_DEFAULT_OPTIONS", () => { - expect(Data.toCBORHex(Data.map([[1n, 2n]]))).toBe("bf0102ff") + it("is the Data default", () => { + expect(Data.DEFAULT_CBOR_OPTIONS).toBe(CBOR.PLUTUS_DATA_OPTIONS) + expect(Data.toCBORHex(Data.map([[1n, 2n]]))).toBe("a10102") + }) + + it("leaves CML_DATA_DEFAULT_OPTIONS writing maps indefinite", () => { + expect(Data.toCBORHex(Data.map([[1n, 2n]]), CBOR.CML_DATA_DEFAULT_OPTIONS)).toBe("bf0102ff") }) }) diff --git a/packages/evolution/test/Data.golden.test.ts b/packages/evolution/test/Data.golden.test.ts index 34a76a68..8ab7e7a6 100644 --- a/packages/evolution/test/Data.golden.test.ts +++ b/packages/evolution/test/Data.golden.test.ts @@ -28,12 +28,17 @@ import * as fs from "fs" import * as path from "path" import { fileURLToPath } from "url" +import * as CBOR from "../src/CBOR.js" import * as Data from "../src/Data.js" // ES module equivalent of __dirname const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) +// The fixtures were generated with the old Data default, which wrote maps +// indefinite. +const GOLDEN_OPTIONS = CBOR.CML_DATA_DEFAULT_OPTIONS + /** * Test configuration defining the number of samples to test for each category * @@ -233,7 +238,7 @@ describe("Data Golden Tests", () => { throw new Error(`Invalid integer sample at index ${entry.index}`) } const plutusData = Data.int(entry.sample.integer) - const encoded = Data.toCBORHex(plutusData) + const encoded = Data.toCBORHex(plutusData, GOLDEN_OPTIONS) expect(encoded, `Failed at sample index ${entry.index}`).toBe(entry.cborHex) }) }) @@ -246,7 +251,7 @@ describe("Data Golden Tests", () => { throw new Error(`Invalid integer sample at index ${entry.index}`) } const plutusData = Data.int(entry.sample.integer) - const encoded = Data.toCBORBytes(plutusData) + const encoded = Data.toCBORBytes(plutusData, GOLDEN_OPTIONS) expect(Array.from(encoded), `Failed at sample index ${entry.index}`).toEqual(entry.cborBytes) }) }) @@ -277,7 +282,7 @@ describe("Data Golden Tests", () => { throw new Error(`Invalid integer sample at index ${entry.index}`) } const plutusData = Data.int(entry.sample.integer) - const encoded = Data.toCBORHex(plutusData) + const encoded = Data.toCBORHex(plutusData, GOLDEN_OPTIONS) const decoded = Data.fromCBORHex(encoded) expect(normalizeDecodedData(decoded), `Failed at sample index ${entry.index}`).toEqual(entry.sample) @@ -294,7 +299,7 @@ describe("Data Golden Tests", () => { throw new Error(`Invalid byte array sample at index ${entry.index}`) } const plutusData = Data.bytearray(entry.sample.bytearray) - const encoded = Data.toCBORHex(plutusData) + const encoded = Data.toCBORHex(plutusData, GOLDEN_OPTIONS) expect(encoded, `Failed at sample index ${entry.index}`).toBe(entry.cborHex) }) }) @@ -307,7 +312,7 @@ describe("Data Golden Tests", () => { throw new Error(`Invalid byte array sample at index ${entry.index}`) } const plutusData = Data.bytearray(entry.sample.bytearray) - const encoded = Data.toCBORBytes(plutusData) + const encoded = Data.toCBORBytes(plutusData, GOLDEN_OPTIONS) expect(Array.from(encoded), `Failed at sample index ${entry.index}`).toEqual(entry.cborBytes) }) }) @@ -338,7 +343,7 @@ describe("Data Golden Tests", () => { throw new Error(`Invalid byte array sample at index ${entry.index}`) } const plutusData = Data.bytearray(entry.sample.bytearray) - const encoded = Data.toCBORHex(plutusData) + const encoded = Data.toCBORHex(plutusData, GOLDEN_OPTIONS) const decoded = Data.fromCBORHex(encoded) expect(normalizeDecodedData(decoded), `Failed at sample index ${entry.index}`).toEqual(entry.sample) @@ -356,7 +361,7 @@ describe("Data Golden Tests", () => { } const plutusDataList = entry.sample.list.map(reconstructPlutusData) const plutusData = Data.list(plutusDataList) - const encoded = Data.toCBORHex(plutusData) + const encoded = Data.toCBORHex(plutusData, GOLDEN_OPTIONS) expect(encoded, `Failed at sample index ${entry.index}`).toBe(entry.cborHex) }) }, 30000) // 30 second timeout for large test cases @@ -370,7 +375,7 @@ describe("Data Golden Tests", () => { } const plutusDataList = entry.sample.list.map(reconstructPlutusData) const plutusData = Data.list(plutusDataList) - const encoded = Data.toCBORBytes(plutusData) + const encoded = Data.toCBORBytes(plutusData, GOLDEN_OPTIONS) expect(Array.from(encoded), `Failed at sample index ${entry.index}`).toEqual(entry.cborBytes) }) }) @@ -402,7 +407,7 @@ describe("Data Golden Tests", () => { } const plutusDataList = entry.sample.list.map(reconstructPlutusData) const plutusData = Data.list(plutusDataList) - const encoded = Data.toCBORHex(plutusData) + const encoded = Data.toCBORHex(plutusData, GOLDEN_OPTIONS) const decoded = Data.fromCBORHex(encoded) expect(normalizeDecodedData(decoded), `Failed at sample index ${entry.index}`).toEqual(entry.sample) @@ -423,7 +428,7 @@ describe("Data Golden Tests", () => { [reconstructPlutusData(entryObj.key), reconstructPlutusData(entryObj.value)] as [Data.Data, Data.Data] ) const plutusData = Data.map(plutusDataEntries) - const encoded = Data.toCBORHex(plutusData) + const encoded = Data.toCBORHex(plutusData, GOLDEN_OPTIONS) expect(encoded, `Failed at sample index ${entry.index}`).toBe(entry.cborHex) }) }) @@ -440,7 +445,7 @@ describe("Data Golden Tests", () => { [reconstructPlutusData(entryObj.key), reconstructPlutusData(entryObj.value)] as [Data.Data, Data.Data] ) const plutusData = Data.map(plutusDataEntries) - const encoded = Data.toCBORBytes(plutusData) + const encoded = Data.toCBORBytes(plutusData, GOLDEN_OPTIONS) expect(Array.from(encoded), `Failed at sample index ${entry.index}`).toEqual(entry.cborBytes) }) }) @@ -475,7 +480,7 @@ describe("Data Golden Tests", () => { [reconstructPlutusData(entryObj.key), reconstructPlutusData(entryObj.value)] as [Data.Data, Data.Data] ) const plutusData = Data.map(plutusDataEntries) - const encoded = Data.toCBORHex(plutusData) + const encoded = Data.toCBORHex(plutusData, GOLDEN_OPTIONS) const decoded = Data.fromCBORHex(encoded) expect(normalizeDecodedData(decoded), `Failed at sample index ${entry.index}`).toEqual(entry.sample) @@ -493,7 +498,7 @@ describe("Data Golden Tests", () => { } const plutusDataFields = entry.sample.fields.map(reconstructPlutusData) const plutusData = Data.constr(BigInt(entry.sample.index), plutusDataFields) - const encoded = Data.toCBORHex(plutusData) + const encoded = Data.toCBORHex(plutusData, GOLDEN_OPTIONS) expect(encoded, `Failed at sample index ${entry.index}`).toBe(entry.cborHex) }) }) @@ -507,7 +512,7 @@ describe("Data Golden Tests", () => { } const plutusDataFields = entry.sample.fields.map(reconstructPlutusData) const plutusData = Data.constr(BigInt(entry.sample.index), plutusDataFields) - const encoded = Data.toCBORBytes(plutusData) + const encoded = Data.toCBORBytes(plutusData, GOLDEN_OPTIONS) expect(Array.from(encoded), `Failed at sample index ${entry.index}`).toEqual(entry.cborBytes) }) }) @@ -539,7 +544,7 @@ describe("Data Golden Tests", () => { } const plutusDataFields = entry.sample.fields.map(reconstructPlutusData) const plutusData = Data.constr(BigInt(entry.sample.index), plutusDataFields) - const encoded = Data.toCBORHex(plutusData) + const encoded = Data.toCBORHex(plutusData, GOLDEN_OPTIONS) const decoded = Data.fromCBORHex(encoded) expect(normalizeDecodedData(decoded), `Failed at sample index ${entry.index}`).toEqual(entry.sample) @@ -553,7 +558,7 @@ describe("Data Golden Tests", () => { testCases.forEach((entry) => { const plutusData = reconstructPlutusData(entry.sample) - const encoded = Data.toCBORHex(plutusData) + const encoded = Data.toCBORHex(plutusData, GOLDEN_OPTIONS) expect(encoded, `Failed at sample index ${entry.index}`).toBe(entry.cborHex) }) }) @@ -563,7 +568,7 @@ describe("Data Golden Tests", () => { testCases.forEach((entry) => { const plutusData = reconstructPlutusData(entry.sample) - const encoded = Data.toCBORBytes(plutusData) + const encoded = Data.toCBORBytes(plutusData, GOLDEN_OPTIONS) expect(Array.from(encoded), `Failed at sample index ${entry.index}`).toEqual(entry.cborBytes) }) }) @@ -591,7 +596,7 @@ describe("Data Golden Tests", () => { testCases.forEach((entry) => { const plutusData = reconstructPlutusData(entry.sample) - const encoded = Data.toCBORHex(plutusData) + const encoded = Data.toCBORHex(plutusData, GOLDEN_OPTIONS) const decoded = Data.fromCBORHex(encoded) expect(normalizeDecodedData(decoded), `Failed at sample index ${entry.index}`).toEqual(entry.sample) diff --git a/packages/evolution/test/Data.test.ts b/packages/evolution/test/Data.test.ts index dbee2324..2bba9216 100644 --- a/packages/evolution/test/Data.test.ts +++ b/packages/evolution/test/Data.test.ts @@ -161,7 +161,7 @@ describe("CBOR Encoding/Decoding", () => { { name: "map with entries", value: Data.map([[1n, Data.bytearray("cafe")]]), - expectedHex: "bf0142cafeff" + expectedHex: "a10142cafe" }, { name: "small int", diff --git a/packages/evolution/test/DatumOption.OriginalBytes.test.ts b/packages/evolution/test/DatumOption.OriginalBytes.test.ts index 172d0eaf..0f7f5619 100644 --- a/packages/evolution/test/DatumOption.OriginalBytes.test.ts +++ b/packages/evolution/test/DatumOption.OriginalBytes.test.ts @@ -110,12 +110,12 @@ describe("inline datum keeps its original bytes", () => { } it("a new InlineDatum with the same Data uses the default encoding", () => { - const option = "8201d818" + bstr("d8799fa10102ff") + const option = "8201d818" + bstr("d8799fbf0102ffff") const decoded = DatumOption.fromCBORHex(option) if (!InlineDatum.isInlineDatum(decoded)) throw new Error("expected an inline datum") const edited = new InlineDatum.InlineDatum({ data: decoded.data }) const expected = "8201d818" + bstr(PlutusData.toCBORHex(decoded.data)) - expect(expected).toBe("8201d818" + bstr("d8799fbf0102ffff")) + expect(expected).toBe("8201d818" + bstr("d8799fa10102ff")) expect(DatumOption.toCBORHex(edited)).toBe(expected) }) }) diff --git a/packages/evolution/test/TSchema.test.ts b/packages/evolution/test/TSchema.test.ts index 0ffbe6f0..e7b6d12e 100644 --- a/packages/evolution/test/TSchema.test.ts +++ b/packages/evolution/test/TSchema.test.ts @@ -170,7 +170,7 @@ describe("TypeTaggedSchema Tests", () => { const encoded = Data.withSchema(IntegerByteArrayMap).toCBORHex(input) const decoded = Data.withSchema(IntegerByteArrayMap).fromCBORHex(encoded) - expect(encoded).toEqual("bf190c894231311b0000003a06945f464432323232ff") + expect(encoded).toEqual("a2190c894231311b0000003a06945f464432323232") expect(eq(decoded, input)).toBe(true) }) From e76c4758b4f6536eca53807bd7cbf0f96e6d2166 Mon Sep 17 00:00:00 2001 From: solidsnakedev Date: Fri, 9 Oct 2026 09:47:35 -0600 Subject: [PATCH 3/4] docs: document the Plutus data default layout --- docs/content/docs/encoding/cbor.mdx | 4 ++-- docs/content/docs/encoding/data.mdx | 4 +++- .../docs/introduction/important-defaults.mdx | 15 ++++++++++++--- .../content/docs/smart-contracts/apply-params.mdx | 4 ++-- 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/docs/content/docs/encoding/cbor.mdx b/docs/content/docs/encoding/cbor.mdx index 6026a71b..7a9bc68a 100644 --- a/docs/content/docs/encoding/cbor.mdx +++ b/docs/content/docs/encoding/cbor.mdx @@ -123,8 +123,8 @@ 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 | -| `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 | +| `PLUTUS_DATA_OPTIONS` | Plutus data in the node layout: indefinite lists and constructor fields, definite maps. The default for `Data` encoding, `UPLC.applyParamsToScript` and `UPLC.dataConstant`; matches `cardano-cli`, Aiken `cbor.serialise()`, and `aiken blueprint apply` | +| `CML_DATA_DEFAULT_OPTIONS` | Deprecated. Indefinite lists, constructor fields and maps, keys in insertion order. The `Data` default before `PLUTUS_DATA_OPTIONS`; use it only to reproduce datum bytes and hashes the SDK wrote before. It matches no tool exactly: CML `PlutusData.to_cardano_node_format()` also sorts map keys | | `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` | diff --git a/docs/content/docs/encoding/data.mdx b/docs/content/docs/encoding/data.mdx index 10f82b2a..90137a69 100644 --- a/docs/content/docs/encoding/data.mdx +++ b/docs/content/docs/encoding/data.mdx @@ -199,7 +199,7 @@ const datum = Data.constr(0n, [ // Encode to hex string const cborHex = Data.toCBORHex(datum) -// "d8799fa2646265..." +// "d8799fa24b62656e..." // Encode to bytes const cborBytes = Data.toCBORBytes(datum) @@ -210,6 +210,8 @@ const decoded = Data.fromCBORHex(cborHex) // Returns original PlutusData structure ``` +By default the encoder writes the layout the node writes, `CBOR.PLUTUS_DATA_OPTIONS`: non-empty lists and constructor fields indefinite, maps definite. The map above starts with `a2`, a definite map of two entries. Earlier releases wrote maps indefinite. To reproduce those bytes or datum hashes, pass the deprecated `CBOR.CML_DATA_DEFAULT_OPTIONS` to `Data.toCBORHex`, `Data.toCBORBytes` or `Data.toDatumHash`. + ## Equality Comparison Check if two PlutusData structures are equal: diff --git a/docs/content/docs/introduction/important-defaults.mdx b/docs/content/docs/introduction/important-defaults.mdx index ded8b1b3..7206c35c 100644 --- a/docs/content/docs/introduction/important-defaults.mdx +++ b/docs/content/docs/introduction/important-defaults.mdx @@ -78,16 +78,25 @@ const tx2 = await template.build() // No contamination from tx1 ### CBOR: Aiken-Compatible by Default -`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. +Plutus data is encoded 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. The default covers `Data.toCBORHex()`, `Data.toDatumHash()`, inline and witness datums, redeemer data, and the parameters of `UPLC.applyParamsToScript()`. If you're using a different Plutus toolchain, you may need different options. ```typescript // Default: node layout, same as aiken blueprint apply UPLC.applyParamsToScript(script, params) -// Indefinite lists and maps, like CML to_cardano_node_format() (map keys keep insertion order): -UPLC.applyParamsToScript(script, params, CBOR.CML_DATA_DEFAULT_OPTIONS) +// Definite lists and maps, like CML to_cbor_hex() on new data: +UPLC.applyParamsToScript(script, params, CBOR.CML_DATA_DEFINITE_OPTIONS) ``` +Before this default, `Data` wrote non-empty maps indefinite, so a datum that holds a map had different bytes and a different datum hash. To reproduce those, pass the deprecated `CBOR.CML_DATA_DEFAULT_OPTIONS`: + +```typescript +Data.toDatumHash(datum, CBOR.CML_DATA_DEFAULT_OPTIONS) +Transaction.toCBORHex(tx, { ledger: CBOR.CML_DEFAULT_OPTIONS, plutusData: CBOR.CML_DATA_DEFAULT_OPTIONS }) +``` + +The transaction options cover witness datums and redeemer data. An inline datum in a new output always uses the `Data` default. + ### Scripts Are Double-CBOR Encoded Compiled Plutus scripts from Aiken/Plutarch are typically double-CBOR encoded (flat bytes → CBOR bytes → CBOR bytes). The SDK handles this automatically, but if you're working with raw script bytes, be aware of the encoding level. diff --git a/docs/content/docs/smart-contracts/apply-params.mdx b/docs/content/docs/smart-contracts/apply-params.mdx index 2f26266f..e0ea2a37 100644 --- a/docs/content/docs/smart-contracts/apply-params.mdx +++ b/docs/content/docs/smart-contracts/apply-params.mdx @@ -134,11 +134,11 @@ const applied = UPLC.applyParamsToScript(compiledScript, [ Data.int(42n), ]) -// With indefinite lists and maps, like CML to_cardano_node_format() (map keys keep insertion order) +// With definite lists and maps, like CML to_cbor_hex() on new data const appliedCml = UPLC.applyParamsToScript( compiledScript, [Data.int(42n)], - CBOR.CML_DATA_DEFAULT_OPTIONS, + CBOR.CML_DATA_DEFINITE_OPTIONS, ) ``` From 8830d031371b625d663da437222a88947c96a4fb Mon Sep 17 00:00:00 2001 From: solidsnakedev Date: Fri, 9 Oct 2026 09:47:35 -0600 Subject: [PATCH 4/4] release: changeset for the Plutus data default --- .changeset/data-default-plutus-layout.md | 24 ++++++++++++++++++++++++ .changeset/plutus-data-tx-options.md | 6 +++--- 2 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 .changeset/data-default-plutus-layout.md diff --git a/.changeset/data-default-plutus-layout.md b/.changeset/data-default-plutus-layout.md new file mode 100644 index 00000000..de2f7f79 --- /dev/null +++ b/.changeset/data-default-plutus-layout.md @@ -0,0 +1,24 @@ +--- +"@evolution-sdk/evolution": minor +--- + +Plutus data is now encoded by default with `CBOR.PLUTUS_DATA_OPTIONS`, the layout the node writes: non-empty lists and constructor fields indefinite, maps definite. Before, the default was `CBOR.CML_DATA_DEFAULT_OPTIONS`, which also wrote non-empty maps indefinite. `Data.DEFAULT_CBOR_OPTIONS` and `CBOR.TX_DEFAULT_OPTIONS.plutusData` are now `CBOR.PLUTUS_DATA_OPTIONS`, and `CBOR.toTxCodecOptions` maps `CBOR.CML_DEFAULT_OPTIONS` to it. + +Only data that holds a non-empty map changes, and only in its map headers. `Data.map([[1n, 2n]])` was `bf0102ff` and is now `a10102`. For such data these change: + +- datum hashes from `Data.toDatumHash`, which now match the hashes the node computes +- inline datums, witness datums and redeemer data in new transactions +- script data hashes of transactions that carry such datums or redeemers + +Applied script hashes do not change, since `UPLC.applyParamsToScript` already used `CBOR.PLUTUS_DATA_OPTIONS`. Decoding is unchanged, and a decoded transaction keeps the original bytes of its datums and redeemers. + +To keep the old bytes, pass the old options: + +```ts +Data.toDatumHash(datum, CBOR.CML_DATA_DEFAULT_OPTIONS) +Transaction.toCBORHex(tx, { ledger: CBOR.CML_DEFAULT_OPTIONS, plutusData: CBOR.CML_DATA_DEFAULT_OPTIONS }) +``` + +The transaction options cover witness datums and redeemer data. An inline datum in a new output is written with the `Data` default under every option set, as before, so it takes the new layout. + +`CBOR.CML_DATA_DEFAULT_OPTIONS` is deprecated in favor of `CBOR.PLUTUS_DATA_OPTIONS`. It keeps its bytes, but matches no tool exactly, so use it only to reproduce bytes and datum hashes written before this release. diff --git a/.changeset/plutus-data-tx-options.md b/.changeset/plutus-data-tx-options.md index 44f571f6..3b799d9c 100644 --- a/.changeset/plutus-data-tx-options.md +++ b/.changeset/plutus-data-tx-options.md @@ -6,7 +6,7 @@ Witness datums and redeemer data are now written with their own Plutus data opti `CBOR.TxCodecOptions` holds two sets of options. `ledger` is for ledger structures: the body, the witness set, and the containers that hold datums and redeemers. `plutusData` is for Plutus data items: witness datums and redeemer data. Two presets come with it: -- `CBOR.TX_DEFAULT_OPTIONS`: `ledger` is `CBOR.CML_DEFAULT_OPTIONS` and `plutusData` is `CBOR.CML_DATA_DEFAULT_OPTIONS`, the `Data` default. +- `CBOR.TX_DEFAULT_OPTIONS`: `ledger` is `CBOR.CML_DEFAULT_OPTIONS` and `plutusData` is `CBOR.PLUTUS_DATA_OPTIONS`, the `Data` default. - `CBOR.TX_CANONICAL_OPTIONS`: `CBOR.CANONICAL_OPTIONS` for both. The encoders and schemas of `Transaction`, `TransactionWitnessSet`, `Redeemers` and `Redeemer`, and `Redeemers.toScriptDataHash`, take `CBOR.TxCodecOptions` and default to `CBOR.TX_DEFAULT_OPTIONS`: @@ -17,6 +17,6 @@ Transaction.toCBORHex(tx, CBOR.TX_CANONICAL_OPTIONS) Transaction.toCBORHex(tx, { ledger: CBOR.CML_DEFAULT_OPTIONS, plutusData: CBOR.PLUTUS_DATA_OPTIONS }) ``` -Plain `CBOR.CodecOptions` are still accepted, read as `CBOR.toTxCodecOptions` reads them: the options serve as both, except that `CBOR.CML_DEFAULT_OPTIONS` writes Plutus data with `CBOR.CML_DATA_DEFAULT_OPTIONS`, as passing no options does. Decoded datums and redeemers keep their bytes, and a datum or redeemer added to a decoded transaction is written with the default Plutus data options. +Plain `CBOR.CodecOptions` are still accepted, read as `CBOR.toTxCodecOptions` reads them: the options serve as both, except that `CBOR.CML_DEFAULT_OPTIONS` writes Plutus data with `CBOR.PLUTUS_DATA_OPTIONS`, as passing no options does. Decoded datums and redeemers keep their bytes, and a datum or redeemer added to a decoded transaction is written with the default Plutus data options. -Script transactions whose redeemer data or witness datums hold a non-empty list, map or constructor fields change bytes. Simple data such as `Data.constr(0n, [])` or an integer keeps its bytes. The builder wrote redeemer data definite, for example `d87982a101028103`, and now writes it in the data default with indefinite lists, maps and constructor fields: `d8799fbf0102ff9f03ffff`. To keep the old redeemer layout, pass `{ ledger: CBOR.CML_DEFAULT_OPTIONS, plutusData: CBOR.CML_DATA_DEFINITE_OPTIONS }` to the encoders and to `Redeemers.toScriptDataHash`. +Script transactions whose redeemer data or witness datums hold a non-empty list, map or constructor fields change bytes. Simple data such as `Data.constr(0n, [])` or an integer keeps its bytes. The builder wrote redeemer data definite, for example `d87982a101028103`, and now writes it in the data default with indefinite lists and constructor fields and a definite map: `d8799fa101029f03ffff`. To keep the old redeemer layout, pass `{ ledger: CBOR.CML_DEFAULT_OPTIONS, plutusData: CBOR.CML_DATA_DEFINITE_OPTIONS }` to the encoders and to `Redeemers.toScriptDataHash`.