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
5 changes: 5 additions & 0 deletions .changeset/script-data-hash-datum-options.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@evolution-sdk/evolution": patch
---

`Redeemers.toScriptDataHash` now encodes datums with the codec options it is given. Before, it applied the options to the redeemers only and always wrote datums in the default Plutus data encoding. A hash requested with `CBOR.CANONICAL_OPTIONS` then did not match a witness set holding canonical datums, and the node rejected the transaction. Calls without options produce the same hashes as before.
6 changes: 4 additions & 2 deletions packages/evolution/src/Redeemers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,8 @@ export const toScriptDataHash = (
options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS
): ScriptDataHash.ScriptDataHash => {
const hasDatums = Array.isArray(datums) && datums.length > 0
// Datums keep their own default encoding unless the caller chose options
const datumOptions = options === CBOR.CML_DEFAULT_OPTIONS ? CBOR.CML_DATA_DEFAULT_OPTIONS : options

// Language views encoding (handles PlutusV1 indefinite-length quirk per spec)
const langViewsBytes = CostModel.languageViewsEncoding(costModels)
Expand All @@ -597,7 +599,7 @@ export const toScriptDataHash = (

if (hasDatums && redeemers.size === 0) {
// Special case (CDDL): [ A0 | tag(258) datums | A0 ]
const datumsBytes = encodeDatumsTaggedSet(datums)
const datumsBytes = encodeDatumsTaggedSet(datums, datumOptions)
payload = concatBytes(
new Uint8Array([0xa0]), // Empty map
datumsBytes,
Expand All @@ -609,7 +611,7 @@ export const toScriptDataHash = (
redeemers._tag === "RedeemerMap"
? toCBORBytesMap(redeemers, options)
: toCBORBytes(redeemers as RedeemerArray, options)
const datumsBytes = hasDatums ? encodeDatumsTaggedSet(datums) : undefined
const datumsBytes = hasDatums ? encodeDatumsTaggedSet(datums, datumOptions) : undefined

payload = datumsBytes
? concatBytes(redeemersBytes, datumsBytes, langViewsBytes)
Expand Down
63 changes: 63 additions & 0 deletions packages/evolution/test/UtilsHash.CML.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import * as CML from "@dcspark/cardano-multiplatform-lib-nodejs"
import { blake2b } from "@noble/hashes/blake2.js"
import { FastCheck, Schema } from "effect"
import { describe, expect, it } from "vitest"

import * as AuxiliaryData from "../src/AuxiliaryData.js"
import * as AuxiliaryDataHash from "../src/AuxiliaryDataHash.js"
import * as Bytes from "../src/Bytes.js"
import * as CBOR from "../src/CBOR.js"
import * as CostModel from "../src/CostModel.js"
import * as Data from "../src/Data.js"
Expand Down Expand Up @@ -277,4 +279,65 @@ describe("UtilsHash helpers CML parity", () => {

expect(evolutionHex).toBe(cmlHex)
})

describe("codec options reach the datums", () => {
const datumHex = "d8799f019f02ffff" // Constr(0, [1, [2]]) with indefinite lists
const datum = Data.fromCBORHex(datumHex)
const canonicalDatumHex = Data.toCBORHex(datum, CBOR.CANONICAL_OPTIONS)
const noCostModels = new CostModel.CostModels({
PlutusV1: new CostModel.CostModel({ costs: [] }),
PlutusV2: new CostModel.CostModel({ costs: [] }),
PlutusV3: new CostModel.CostModel({ costs: [] })
})
const datumList = (hex: string) => {
const list = CML.PlutusDataList.new()
list.add(CML.PlutusData.from_cbor_hex(hex))
return list
}

it("hashes canonical datums when there are no redeemers", () => {
expect(canonicalDatumHex).toBe("d87982018102")
const redeemers = new Redeemers.RedeemerArray({ value: [] })
const evolutionHex = ScriptDataHash.toHex(
Redeemers.toScriptDataHash(redeemers, noCostModels, [datum], CBOR.CANONICAL_OPTIONS)
)
const cmlHex = CML.hash_script_data(
CML.Redeemers.from_cbor_hex("80"),
CML.CostModels.from_cbor_hex("a0"),
datumList(canonicalDatumHex)
).to_hex()
expect(evolutionHex).toBe(cmlHex)
expect(evolutionHex).toBe(toHex(blake2b(Bytes.fromHex(`a0d9010281${canonicalDatumHex}a0`), { dkLen: 32 })))
})

it("hashes canonical datums next to redeemers", () => {
const redeemer = new Redeemer.Redeemer({
tag: "spend",
index: 0n,
data: Data.constr(0n, []),
exUnits: new Redeemer.ExUnits({ mem: 1n, steps: 1n })
})
const redeemers = new Redeemers.RedeemerArray({ value: [redeemer] })
const evolutionHex = ScriptDataHash.toHex(
Redeemers.toScriptDataHash(redeemers, noCostModels, [datum], CBOR.CANONICAL_OPTIONS)
)
const cmlHex = CML.hash_script_data(
CML.Redeemers.from_cbor_hex(Redeemers.toCBORHex(redeemers, CBOR.CANONICAL_OPTIONS)),
CML.CostModels.from_cbor_hex("a0"),
datumList(canonicalDatumHex)
).to_hex()
expect(evolutionHex).toBe(cmlHex)
})

it("keeps the default datum encoding when no options are given", () => {
const redeemers = new Redeemers.RedeemerArray({ value: [] })
const evolutionHex = ScriptDataHash.toHex(Redeemers.toScriptDataHash(redeemers, noCostModels, [datum]))
const cmlHex = CML.hash_script_data(
CML.Redeemers.from_cbor_hex("80"),
CML.CostModels.from_cbor_hex("a0"),
datumList(datumHex)
).to_hex()
expect(evolutionHex).toBe(cmlHex)
})
})
})
Loading