Skip to content

Commit 4e748e6

Browse files
feat(datum): write new inline datums with the plutusData codec options (#618)
* feat(datum): write new inline datums with the plutusData codec options * test: cover inline datums under TxCodecOptions * docs: inline datums follow the plutusData codec options * release: changeset for inline datum codec options
1 parent 98e8b38 commit 4e748e6

13 files changed

Lines changed: 1026 additions & 468 deletions

‎.changeset/data-default-plutus-layout.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,6 @@ Data.toDatumHash(datum, CBOR.CML_DATA_DEFAULT_OPTIONS)
1919
Transaction.toCBORHex(tx, { ledger: CBOR.CML_DEFAULT_OPTIONS, plutusData: CBOR.CML_DATA_DEFAULT_OPTIONS })
2020
```
2121

22-
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.
22+
The transaction options cover inline datums, witness datums and redeemer data, so the second call keeps the old bytes for all three.
2323

2424
`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.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
"@evolution-sdk/evolution": minor
3+
---
4+
5+
A new inline datum is now written with the `plutusData` transaction options, as witness datums and redeemer data are. Before, it was written with `Data.DEFAULT_CBOR_OPTIONS` whatever options the caller passed to the transaction encoder.
6+
7+
```ts
8+
const datum = Data.map([[1n, 2n]])
9+
// Before: 24(h'a10102') under every option set
10+
// Now: 24(h'bf0102ff'), the bytes Data.toCBORHex(datum, CBOR.CML_DATA_DEFAULT_OPTIONS) gives
11+
Transaction.toCBORHex(tx, { ledger: CBOR.CML_DEFAULT_OPTIONS, plutusData: CBOR.CML_DATA_DEFAULT_OPTIONS })
12+
```
13+
14+
The encoders and schemas of `TransactionBody`, `TxOut`, `TransactionOutput` and `DatumOption` take `CBOR.TxCodecOptions` and default to `CBOR.TX_DEFAULT_OPTIONS`. Plain `CBOR.CodecOptions` are still accepted, read as `CBOR.toTxCodecOptions` reads them. `DatumOption.makeFromCDDL` and `TxOut.makeFromCDDL` build the CDDL schema for given `plutusData` options.
15+
16+
With no options, `CBOR.TX_DEFAULT_OPTIONS` or `CBOR.CML_DEFAULT_OPTIONS`, every byte stays the same. Under other options only the bytes inside tag 24 change, so the transaction id of a transaction with a new inline datum changes. A decoded inline datum keeps its bytes, and an inline datum added to a decoded transaction is written with the default options.

‎.changeset/plutus-data-tx-options.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
Witness datums and redeemer data are now written with their own Plutus data options. Before, the witness set wrote them with the transaction options, so a datum in the witness set did not match the bytes its datum hash covers.
66

7-
`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:
7+
`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: new inline datums, witness datums and redeemer data. Two presets come with it:
88

99
- `CBOR.TX_DEFAULT_OPTIONS`: `ledger` is `CBOR.CML_DEFAULT_OPTIONS` and `plutusData` is `CBOR.PLUTUS_DATA_OPTIONS`, the `Data` default.
1010
- `CBOR.TX_CANONICAL_OPTIONS`: `CBOR.CANONICAL_OPTIONS` for both.
@@ -19,4 +19,4 @@ Transaction.toCBORHex(tx, { ledger: CBOR.CML_DEFAULT_OPTIONS, plutusData: CBOR.P
1919

2020
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.
2121

22-
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`.
22+
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`. That call also writes new inline datums fully definite.

‎docs/content/docs/introduction/important-defaults.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ Data.toDatumHash(datum, CBOR.CML_DATA_DEFAULT_OPTIONS)
9595
Transaction.toCBORHex(tx, { ledger: CBOR.CML_DEFAULT_OPTIONS, plutusData: CBOR.CML_DATA_DEFAULT_OPTIONS })
9696
```
9797

98-
The transaction options cover witness datums and redeemer data. An inline datum in a new output always uses the `Data` default.
98+
The `plutusData` transaction options cover every new Plutus data item: inline datums, witness datums and redeemer data. A datum decoded from a transaction keeps its bytes.
9999

100100
### Scripts Are Double-CBOR Encoded
101101

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/**
2+
* Devnet test: a new inline datum is written with the plutusData transaction
3+
* options, and the node keeps it as written.
4+
*
5+
* The builder writes a transaction with an inline map datum. Each case
6+
* rebuilds that transaction with a new InlineDatum, encodes it with
7+
* Transaction.toCBORHex under one option set, signs it over the raw body with
8+
* Transaction.addVKeyWitnessesBytes, and submits the bytes. The ledger returns
9+
* the datum, which must be the bytes Data.toCBORHex writes under the case's
10+
* plutusData options.
11+
*/
12+
13+
import { afterAll, beforeAll, describe, expect, it } from "@effect/vitest"
14+
import * as Cluster from "@evolution-sdk/devnet/Cluster"
15+
import * as Config from "@evolution-sdk/devnet/Config"
16+
import * as Genesis from "@evolution-sdk/devnet/Genesis"
17+
import { Cardano, Client, preprod } from "@evolution-sdk/evolution"
18+
import * as Address from "@evolution-sdk/evolution/Address"
19+
import * as Bytes from "@evolution-sdk/evolution/Bytes"
20+
import * as CBOR from "@evolution-sdk/evolution/CBOR"
21+
import * as Data from "@evolution-sdk/evolution/Data"
22+
import * as InlineDatum from "@evolution-sdk/evolution/InlineDatum"
23+
import * as Transaction from "@evolution-sdk/evolution/Transaction"
24+
import * as TransactionBody from "@evolution-sdk/evolution/TransactionBody"
25+
import * as TransactionHash from "@evolution-sdk/evolution/TransactionHash"
26+
import * as TransactionWitnessSet from "@evolution-sdk/evolution/TransactionWitnessSet"
27+
import * as TxOut from "@evolution-sdk/evolution/TxOut"
28+
29+
describe("Inline datum options (Devnet Submit)", () => {
30+
let devnetCluster: Cluster.Cluster | undefined
31+
let genesisUtxos: ReadonlyArray<Cardano.UTxO.UTxO> = []
32+
33+
const TEST_MNEMONIC =
34+
"test test test test test test test test test test test test test test test test test test test test test test test sauce"
35+
36+
// {1: [2]}: each option set below writes it differently, or with a
37+
// different ledger layout around it
38+
const datum = Data.map([[Data.int(1n), Data.list([Data.int(2n)])]])
39+
40+
const cases: ReadonlyArray<{ name: string; options: CBOR.TxCodecOptions; datumHex: string }> = [
41+
{ name: "CBOR.TX_DEFAULT_OPTIONS", options: CBOR.TX_DEFAULT_OPTIONS, datumHex: "a1019f02ff" },
42+
{ name: "CBOR.TX_CANONICAL_OPTIONS", options: CBOR.TX_CANONICAL_OPTIONS, datumHex: "a1018102" },
43+
{
44+
name: "plutusData CML_DATA_DEFAULT_OPTIONS",
45+
options: { ledger: CBOR.CML_DEFAULT_OPTIONS, plutusData: CBOR.CML_DATA_DEFAULT_OPTIONS },
46+
datumHex: "bf019f02ffff"
47+
},
48+
{
49+
name: "plutusData CML_DATA_DEFINITE_OPTIONS",
50+
options: { ledger: CBOR.CML_DEFAULT_OPTIONS, plutusData: CBOR.CML_DATA_DEFINITE_OPTIONS },
51+
datumHex: "a1018102"
52+
}
53+
]
54+
55+
const ogmiosUrl = () => `http://localhost:${devnetCluster!.ports.ogmios}`
56+
57+
const createTestClient = () => {
58+
if (!devnetCluster) throw new Error("Cluster not initialized")
59+
return Client.make(Cluster.getChain(devnetCluster))
60+
.withKupmios({ kupoUrl: `http://localhost:${devnetCluster.ports.kupo}`, ogmiosUrl: ogmiosUrl() })
61+
.withSeed({ mnemonic: TEST_MNEMONIC, accountIndex: 0, addressType: "Base" })
62+
}
63+
64+
const ogmios = async (method: string, params: unknown) => {
65+
const response = await fetch(ogmiosUrl(), {
66+
method: "POST",
67+
headers: { "Content-Type": "application/json" },
68+
body: JSON.stringify({ jsonrpc: "2.0", method, params, id: null })
69+
})
70+
return (await response.json()) as { result?: any; error?: unknown }
71+
}
72+
73+
beforeAll(async () => {
74+
const tempClient = Client.make(preprod).withSeed({ mnemonic: TEST_MNEMONIC, accountIndex: 0, addressType: "Base" })
75+
const testAddressHex = Address.toHex(await tempClient.address())
76+
77+
const genesisConfig: Config.ShelleyGenesis = {
78+
...Config.DEFAULT_SHELLEY_GENESIS,
79+
slotLength: 0.02,
80+
epochLength: 50,
81+
activeSlotsCoeff: 1.0,
82+
initialFunds: { [testAddressHex]: 500_000_000_000 }
83+
}
84+
85+
genesisUtxos = await Genesis.calculateUtxosFromConfig(genesisConfig)
86+
87+
devnetCluster = await Cluster.make({
88+
clusterName: "inline-datum-options-test",
89+
shelleyGenesis: genesisConfig,
90+
kupo: { enabled: true, logLevel: "Info" },
91+
ogmios: { enabled: true, logLevel: "info" }
92+
})
93+
94+
await Cluster.start(devnetCluster)
95+
await new Promise((resolve) => setTimeout(resolve, 3_000))
96+
}, 180_000)
97+
98+
afterAll(async () => {
99+
if (devnetCluster) {
100+
await Cluster.stop(devnetCluster)
101+
await Cluster.remove(devnetCluster)
102+
}
103+
}, 60_000)
104+
105+
cases.forEach(({ datumHex, name, options }, caseIndex) => {
106+
it(`writes and keeps an inline map datum under ${name}`, { timeout: 120_000 }, async () => {
107+
const client = createTestClient()
108+
const myAddress = await client.address()
109+
expect(Data.toCBORHex(datum, options.plutusData)).toBe(datumHex)
110+
111+
const signBuilder = await client
112+
.newTx()
113+
.payToAddress({
114+
address: myAddress,
115+
assets: Cardano.Assets.fromLovelace(5_000_000n),
116+
datum: new InlineDatum.InlineDatum({ data: datum })
117+
})
118+
.build(caseIndex === 0 ? { availableUtxos: [...genesisUtxos] } : undefined)
119+
const built = await signBuilder.toTransaction()
120+
121+
// A new InlineDatum in each output that holds one, and a fee that
122+
// covers the few extra bytes, paid from the largest output
123+
const feeBump = 10_000n
124+
const changeIndex = built.body.outputs.reduce(
125+
(best, output, i) =>
126+
Cardano.Assets.lovelaceOf(output.assets) > Cardano.Assets.lovelaceOf(built.body.outputs[best]!.assets)
127+
? i
128+
: best,
129+
0
130+
)
131+
const outputs = built.body.outputs.map(
132+
(output, i) =>
133+
new TxOut.TransactionOutput({
134+
...output,
135+
assets:
136+
i === changeIndex ? Cardano.Assets.subtractLovelace(output.assets, feeBump) : output.assets,
137+
datumOption:
138+
output.datumOption?._tag === "InlineDatum"
139+
? new InlineDatum.InlineDatum({ data: output.datumOption.data })
140+
: output.datumOption
141+
})
142+
)
143+
const datumIndex = outputs.findIndex((output) => output.datumOption?._tag === "InlineDatum")
144+
expect(datumIndex).toBeGreaterThanOrEqual(0)
145+
const tx = new Transaction.Transaction({
146+
...built,
147+
body: new TransactionBody.TransactionBody({ ...built.body, outputs, fee: built.body.fee + feeBump })
148+
})
149+
150+
const txHex = Transaction.toCBORHex(tx, options)
151+
expect(txHex).toContain(`d818${(0x40 + datumHex.length / 2).toString(16)}${datumHex}`)
152+
const txBytes = Bytes.fromHex(txHex)
153+
const bodyBytes = Transaction.extractBodyBytes(txBytes)
154+
const expectedTxId = TransactionHash.toHex(TransactionBody.toHashFromBytes(bodyBytes))
155+
156+
const walletWitnessSet = await client.signTx(txHex, {
157+
utxos: caseIndex === 0 ? [...genesisUtxos] : await client.getUtxos(myAddress)
158+
})
159+
const signedBytes = Transaction.addVKeyWitnessesBytes(txBytes, TransactionWitnessSet.toCBORBytes(walletWitnessSet))
160+
expect(Bytes.toHex(Transaction.extractBodyBytes(signedBytes))).toBe(Bytes.toHex(bodyBytes))
161+
162+
const submitted = await ogmios("submitTransaction", { transaction: { cbor: Bytes.toHex(signedBytes) } })
163+
expect(submitted.error).toBeUndefined()
164+
expect(submitted.result.transaction.id).toBe(expectedTxId)
165+
expect(await client.awaitTx(TransactionHash.fromHex(expectedTxId), 1000)).toBe(true)
166+
167+
const utxos = await ogmios("queryLedgerState/utxo", {
168+
outputReferences: [{ transaction: { id: expectedTxId }, index: datumIndex }]
169+
})
170+
expect((utxos.result as Array<{ datum?: string }>).map((utxo) => utxo.datum)).toEqual([datumHex])
171+
await new Promise((resolve) => setTimeout(resolve, 2_000))
172+
})
173+
})
174+
})

‎packages/evolution/src/CBOR.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -344,8 +344,8 @@ export const CARDANO_NODE_DATA_OPTIONS: CodecOptions = CML_DATA_DEFINITE_OPTIONS
344344
*
345345
* - `ledger`: the options for ledger structures: the body, the witness set,
346346
* and the containers that hold datums and redeemers.
347-
* - `plutusData`: the options for Plutus data items: witness datums and
348-
* redeemer data.
347+
* - `plutusData`: the options for Plutus data items: new inline datums,
348+
* witness datums and redeemer data.
349349
*
350350
* @since 2.0.0
351351
* @category model

0 commit comments

Comments
 (0)