Skip to content
Open
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
43 changes: 43 additions & 0 deletions packages/mesh-common/src/utils/data-hash.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,49 @@
import { blake2b } from "blakejs";

import { toBytes } from "../data";
import { LanguageVersion } from "../types";

export const hashDrepAnchor = (jsonLD: object): string => {
const jsonHash = blake2b(JSON.stringify(jsonLD, null, 2), undefined, 32);
return Buffer.from(jsonHash).toString("hex");
};

/**
* Language tag bytes prepended to the script bytes when hashing a Plutus
* script, as defined by the Cardano ledger (CIP-0057 / plutus script hash):
* script_hash = blake2b_224(tag || script_bytes).
*/
export const PLUTUS_LANGUAGE_TAGS: Record<LanguageVersion, number> = {
V1: 0x01,
V2: 0x02,
V3: 0x03,
};

/**
* Hash a Plutus script from its CBOR hex (the form produced by Aiken
* blueprints and stored in `PlutusScript.code`).
*
* The ledger hashes the serialised script bytes exactly as they appear in
* the CBOR byte string — the bytes must NOT be unwrapped/decoded first.
* Hashing the unwrapped UPLC bytes instead produces a different hash, which
* derives wrong script addresses and policy ids (see MeshJS/mesh#763).
*
* @param scriptCbor The script CBOR hex (e.g. a blueprint's `compiledCode`)
* @param version The Plutus language version of the script
* @returns The 56-character hex script hash
*/
export const hashPlutusScript = (
scriptCbor: string,
version: LanguageVersion,
): string => {
const tag = PLUTUS_LANGUAGE_TAGS[version];
if (tag === undefined) {
throw new Error(`Invalid Plutus script version: ${version}`);
}
const scriptBytes = toBytes(scriptCbor);
const preimage = new Uint8Array(scriptBytes.length + 1);
preimage[0] = tag;
preimage.set(scriptBytes, 1);
const hash = blake2b(preimage, undefined, 28);
return Buffer.from(hash).toString("hex");
};
34 changes: 33 additions & 1 deletion packages/mesh-common/test/utils/data-hash.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { hashDrepAnchor } from "@meshsdk/common";
import { hashDrepAnchor, hashPlutusScript } from "@meshsdk/common";

describe("hashDrepAnchor", () => {
it("with drep object", () => {
Expand Down Expand Up @@ -84,3 +84,35 @@ describe("hashDrepAnchor", () => {
);
});
});

describe("hashPlutusScript", () => {
// Ground truth: Aiken blueprint hashes from mesh-contract's giftcard
// workspace (plutusVersion v3). The ledger hashes the serialised script
// bytes exactly as they appear in the CBOR byte string.
it("hashes a V3 minting policy to its Aiken blueprint hash", () => {
expect(
hashPlutusScript(
"5901ae01010032323232323232232225333005323232323253323300b3001300c3754004264646464a66601e600a0022a66602460226ea801c540085854ccc03cc00c00454ccc048c044dd50038a8010b0b18079baa006132323232533301430170021323253330133009301437540162a666026601260286ea8c8cc004004018894ccc0600045300103d87a80001323253330173375e603860326ea80080504cdd2a40006603600497ae0133004004001301c002301a00115333013300700113371e00402229405854ccc04ccdc3800a4002266e3c0080445281bad3014002375c60240022c602a00264a666020600860226ea800452f5bded8c026eacc054c048dd500099198008009bab3015301630163016301600322533301400114c103d87a80001323232325333015337220140042a66602a66e3c0280084cdd2a4000660326e980052f5c02980103d87a80001330060060033756602c0066eb8c050008c060008c058004dd6180980098079baa007370e90011bae3010300d37540046e1d200016300e300f002300d001300d002300b0013007375400229309b2b1bae0015734aae7555cf2ab9f5740ae855d11",
"V3",
),
).toEqual("401c967008d42885400991f9225715e1c3a8e43757b1fd36a1328195");
});

it("hashes a V3 spending validator to its Aiken blueprint hash", () => {
expect(
hashPlutusScript(
"59011501010032323232323232232232253330063232323232533300b3370e900118061baa001132323232325333013301600213253330113370e6eb4c04c009200113371e00201e2940dd718088008b180a00099299980799b8748008c040dd50008a5eb7bdb1804dd5980a18089baa001323300100137566028602a602a602a602a60226ea8020894ccc04c004530103d87a80001323232325333014337220200042a66602866e3c0400084cdd2a4000660306e980052f5c02980103d87a80001330060060033756602a0066eb8c04c008c05c008c054004c048c04c008c044004c034dd50008b1807980800118070009807001180600098041baa00114984d958dd70009bae0015734aae7555cf2ab9f5740ae855d101",
"V3",
),
).toEqual("b2386630f1b210c58d0e46f132e931b362c3f373685118018e4d956f");
});

it("produces a different hash per language version tag", () => {
expect(
hashPlutusScript(
"59011501010032323232323232232232253330063232323232533300b3370e900118061baa001132323232325333013301600213253330113370e6eb4c04c009200113371e00201e2940dd718088008b180a00099299980799b8748008c040dd50008a5eb7bdb1804dd5980a18089baa001323300100137566028602a602a602a602a60226ea8020894ccc04c004530103d87a80001323232325333014337220200042a66602866e3c0400084cdd2a4000660306e980052f5c02980103d87a80001330060060033756602a0066eb8c04c008c05c008c054004c048c04c008c044004c034dd50008b1807980800118070009807001180600098041baa00114984d958dd70009bae0015734aae7555cf2ab9f5740ae855d101",
"V2",
),
).not.toEqual("b2386630f1b210c58d0e46f132e931b362c3f373685118018e4d956f");
});
});
9 changes: 5 additions & 4 deletions packages/mesh-core-csl/src/core/serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
DeserializedAddress,
DeserializedScript,
emptyTxBuilderBody,
hashPlutusScript,
IDeserializer,
IMeshTxSerializer,
IResolver,
Expand All @@ -29,7 +30,6 @@ import {
import {
castDataToPlutusData,
csl,
deserializePlutusScript,
resolveDataHash,
resolveEd25519KeyHash,
resolvePrivateKey,
Expand Down Expand Up @@ -143,9 +143,10 @@ export class CSLSerializer implements IMeshTxSerializer {
deserializePlutusScript: function (
script: PlutusScript,
): DeserializedScript {
const scriptHash = deserializePlutusScript(script.code, script.version)
.hash()
.to_hex();
// The ledger hashes the serialised script bytes as they appear in
// the CBOR byte string; the deserialized object's hash() unwraps
// the CBOR first and disagrees with Aiken/cardano-cli (#763).
const scriptHash = hashPlutusScript(script.code, script.version);
return { scriptHash, scriptCbor: script.code };
},
},
Expand Down
9 changes: 6 additions & 3 deletions packages/mesh-core-csl/src/deser/resolver.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
BuilderData,
hashPlutusScript,
mnemonicToEntropy,
NativeScript,
PlutusDataType,
Expand All @@ -8,7 +9,6 @@ import {

import {
castDataToPlutusData,
deserializePlutusScript,
fromUTF8,
toAddress,
toBaseAddress,
Expand Down Expand Up @@ -72,11 +72,14 @@ export const resolvePlutusScriptAddress = (
script: PlutusScript,
networkId = 0,
) => {
const plutusScript = deserializePlutusScript(script.code, script.version);
// Hash the serialised script bytes (ledger rule); the deserialized
// object's hash() unwraps the CBOR byte string first and disagrees
// with Aiken/cardano-cli (see MeshJS/mesh#763).
const scriptHash = hashPlutusScript(script.code, script.version);

const enterpriseAddress = csl.EnterpriseAddress.new(
networkId,
csl.Credential.from_scripthash(plutusScript.hash()),
csl.Credential.from_scripthash(csl.ScriptHash.from_hex(scriptHash)),
);

return enterpriseAddress.to_address().to_bech32();
Expand Down
13 changes: 13 additions & 0 deletions packages/mesh-core-csl/test/utils/serializer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,17 @@ describe("CSLSerializer", () => {
const result = serializer.resolver.script.resolveScriptRef(nativeScript);
expect(result).toEqual("d81846820082041864");
});

it("should deserialize plutus script with the ledger script hash", () => {
// Aiken blueprint vector (giftcard redeem validator, Plutus V3): the
// hash must be taken over the serialised script bytes, not the
// CBOR-unwrapped bytes (see MeshJS/mesh#763).
const result = serializer.deserializer.script.deserializePlutusScript({
code: "59011501010032323232323232232232253330063232323232533300b3370e900118061baa001132323232325333013301600213253330113370e6eb4c04c009200113371e00201e2940dd718088008b180a00099299980799b8748008c040dd50008a5eb7bdb1804dd5980a18089baa001323300100137566028602a602a602a602a60226ea8020894ccc04c004530103d87a80001323232325333014337220200042a66602866e3c0400084cdd2a4000660306e980052f5c02980103d87a80001330060060033756602a0066eb8c04c008c05c008c054004c048c04c008c044004c034dd50008b1807980800118070009807001180600098041baa00114984d958dd70009bae0015734aae7555cf2ab9f5740ae855d101",
version: "V3",
});
expect(result.scriptHash).toEqual(
"b2386630f1b210c58d0e46f132e931b362c3f373685118018e4d956f",
);
});
});
9 changes: 6 additions & 3 deletions packages/mesh-core-cst/src/resolvers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { bech32 } from "bech32";
import {
BuilderData,
fromUTF8,
hashPlutusScript,
mnemonicToEntropy,
NativeScript,
PlutusDataType,
Expand All @@ -29,7 +30,6 @@ import {
PoolId,
} from "../types";
import {
deserializePlutusScript,
deserializeTx,
fromBuilderToPlutusData,
toAddress,
Expand Down Expand Up @@ -93,10 +93,13 @@ export const resolvePlutusScriptAddress = (
script: PlutusScript,
networkId = 0,
): string => {
const plutusScript = deserializePlutusScript(script.code, script.version);
// Hash the serialised script bytes (ledger rule); the SDK object's
// hash() unwraps the CBOR byte string first and disagrees with
// Aiken/cardano-cli (see MeshJS/mesh#763).
const scriptHash = hashPlutusScript(script.code, script.version);

const enterpriseAddress = EnterpriseAddress.fromCredentials(networkId, {
hash: plutusScript.hash(),
hash: Hash28ByteBase16(scriptHash),
type: Cardano.CredentialType.ScriptHash,
});

Expand Down
6 changes: 5 additions & 1 deletion packages/mesh-core-cst/src/serializer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
DeserializedAddress,
DeserializedScript,
fromUTF8,
hashPlutusScript,
IDeserializer,
IMeshTxSerializer,
IResolver,
Expand Down Expand Up @@ -300,7 +301,10 @@ export class CardanoSDKSerializer implements IMeshTxSerializer {
}
}
return {
scriptHash: cardanoPlutusScript.hash().toString(),
// The ledger hashes the serialised script bytes as they appear in
// the CBOR byte string; the SDK object hashes the unwrapped bytes,
// which disagrees with Aiken/cardano-cli (see MeshJS/mesh#763).
scriptHash: hashPlutusScript(script.code, script.version),
scriptCbor: cardanoPlutusScript.toCbor().toString(),
};
},
Expand Down
5 changes: 4 additions & 1 deletion packages/mesh-core-cst/test/resolvers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,11 @@ describe("resolvePlutusScriptAddress", () => {
version: "V1",
};

// The address must embed the ledger script hash, taken over the
// serialised script bytes. The previous expected value embedded a
// hash of the CBOR-unwrapped bytes instead (see MeshJS/mesh#763).
expect(resolvePlutusScriptAddress(script, 0)).toEqual(
"addr_test1wpnlxv2xv9a9ucvnvzqakwepzl9ltx7jzgm53av2e9ncv4sysemm8",
"addr_test1wrrdx3xfy2n7rnrcalp37852a8yeyu3pg7rk0lu0c6qvgaq8yg6l2",
);
});
});
Expand Down
14 changes: 14 additions & 0 deletions packages/mesh-core-cst/test/utils/serializer-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -429,4 +429,18 @@ describe("Serialization utils", () => {
"b2040523c8f8b1cf6888c7d5ffdfbb94b8bc2e1465f2ad891fdce923",
);
});

it("should deserialize plutus script with the ledger script hash", () => {
// Aiken blueprint vector (giftcard redeem validator, Plutus V3): the
// hash must be taken over the serialised script bytes, not the
// CBOR-unwrapped bytes (see MeshJS/mesh#763).
const result = serializer.deserializer.script.deserializePlutusScript({
code: "59011501010032323232323232232232253330063232323232533300b3370e900118061baa001132323232325333013301600213253330113370e6eb4c04c009200113371e00201e2940dd718088008b180a00099299980799b8748008c040dd50008a5eb7bdb1804dd5980a18089baa001323300100137566028602a602a602a602a60226ea8020894ccc04c004530103d87a80001323232325333014337220200042a66602866e3c0400084cdd2a4000660306e980052f5c02980103d87a80001330060060033756602a0066eb8c04c008c05c008c054004c048c04c008c044004c034dd50008b1807980800118070009807001180600098041baa00114984d958dd70009bae0015734aae7555cf2ab9f5740ae855d101",
version: "V3",
});

expect(result.scriptHash).toEqual(
"b2386630f1b210c58d0e46f132e931b362c3f373685118018e4d956f",
);
});
});
6 changes: 5 additions & 1 deletion packages/mesh-core/src/utils/resolver.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
BuilderData,
hashPlutusScript,
LanguageVersion,
NativeScript,
PlutusDataType,
Expand Down Expand Up @@ -58,7 +59,10 @@ export const resolveScriptHash = (
if (!version) {
return core.deserializeNativeScript(scriptCode).hash().toString();
}
return core.deserializePlutusScript(scriptCode, version).hash().toString();
// Hash the serialised script bytes (ledger rule); the deserialized
// script object's hash() unwraps the CBOR byte string first and
// disagrees with Aiken/cardano-cli (see MeshJS/mesh#763).
return hashPlutusScript(scriptCode, version);
};

/**
Expand Down
6 changes: 2 additions & 4 deletions packages/mesh-core/src/utils/serializer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
BuilderData,
DeserializedAddress,
hashPlutusScript,
NativeScript,
PlutusDataType,
PlutusScript,
Expand Down Expand Up @@ -56,10 +57,7 @@ export const serializePlutusScript = (
networkId = 0,
isScriptStakeCredential = false,
) => {
const scriptHash = core
.deserializePlutusScript(script.code, script.version)
.hash()
.toString();
const scriptHash = hashPlutusScript(script.code, script.version);
const address = core.scriptHashToBech32(
scriptHash,
stakeCredentialHash,
Expand Down
14 changes: 13 additions & 1 deletion packages/mesh-core/test/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { deserializePoolId } from "../src";
import { deserializePoolId, resolveScriptHash } from "../src";

describe("Util tests", () => {
it("should correctly deserialize a pool id", () => {
Expand All @@ -8,4 +8,16 @@ describe("Util tests", () => {
),
).toBe("f890ff23ad69119de934c5f4f231ff7a154517a93fae043b48b01bb8");
});

it("should resolve a Plutus V3 script hash matching its Aiken blueprint", () => {
// Giftcard redeem validator from mesh-contract's Aiken workspace.
// The ledger hashes the serialised script bytes; hashing the
// CBOR-unwrapped bytes instead gives a different hash (see #763).
expect(
resolveScriptHash(
"59011501010032323232323232232232253330063232323232533300b3370e900118061baa001132323232325333013301600213253330113370e6eb4c04c009200113371e00201e2940dd718088008b180a00099299980799b8748008c040dd50008a5eb7bdb1804dd5980a18089baa001323300100137566028602a602a602a602a60226ea8020894ccc04c004530103d87a80001323232325333014337220200042a66602866e3c0400084cdd2a4000660306e980052f5c02980103d87a80001330060060033756602a0066eb8c04c008c05c008c054004c048c04c008c044004c034dd50008b1807980800118070009807001180600098041baa00114984d958dd70009bae0015734aae7555cf2ab9f5740ae855d101",
"V3",
),
).toBe("b2386630f1b210c58d0e46f132e931b362c3f373685118018e4d956f");
});
});
13 changes: 3 additions & 10 deletions packages/mesh-transaction/src/transaction/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
CIP68_222,
Data,
DEFAULT_REDEEMER_BUDGET,
hashPlutusScript,
hexToString,
IInitiator,
metadataToCip68,
Expand All @@ -26,7 +27,6 @@ import {
Cardano,
CardanoSDKUtil,
deserializeNativeScript,
deserializePlutusScript,
deserializeTx,
fromScriptRef,
Serialization,
Expand Down Expand Up @@ -397,12 +397,7 @@ export class Transaction {
);
if ("code" in forgeScript) {
// Burn plutus script assets with provided script
policyId = deserializePlutusScript(
forgeScript.code,
forgeScript.version,
)
.hash()
.toString();
policyId = hashPlutusScript(forgeScript.code, forgeScript.version);

this.isCollateralNeeded = true;
this.mintPlutusScript(forgeScript)
Expand All @@ -428,9 +423,7 @@ export class Transaction {
}

if ("code" in script) {
policyId = deserializePlutusScript(script.code, script.version)
.hash()
.toString();
policyId = hashPlutusScript(script.code, script.version);

this.isCollateralNeeded = true;
this.mintPlutusScript(script)
Expand Down