From fa914a890164728609cb9243aa2e2e9118db0943 Mon Sep 17 00:00:00 2001 From: junia Date: Sat, 28 Feb 2026 07:08:03 -0500 Subject: [PATCH] test(evolution): add KeyHash and ScriptHash test coverage Add unit and property-based tests for KeyHash and ScriptHash modules, which use @noble/hashes/blake2.js (updated import path in dependency update PR #36). - KeyHash.test.ts: fromHex/toBytes roundtrip, fromPrivateKey, fromVKey, equality, and FastCheck property tests - ScriptHash.test.ts: fromHex/toBytes roundtrip, fromScript for PlutusV1/ V2/V3/NativeScript, CML parity assertions, equality, and FastCheck property tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/evolution/test/KeyHash.test.ts | 99 ++++++++++++++ packages/evolution/test/ScriptHash.test.ts | 147 +++++++++++++++++++++ 2 files changed, 246 insertions(+) create mode 100644 packages/evolution/test/KeyHash.test.ts create mode 100644 packages/evolution/test/ScriptHash.test.ts diff --git a/packages/evolution/test/KeyHash.test.ts b/packages/evolution/test/KeyHash.test.ts new file mode 100644 index 00000000..17ebb53f --- /dev/null +++ b/packages/evolution/test/KeyHash.test.ts @@ -0,0 +1,99 @@ +import { Equal, FastCheck } from "effect" +import { describe, expect, it } from "vitest" + +import * as Bytes from "../src/Bytes.js" +import * as KeyHash from "../src/KeyHash.js" +import * as PrivateKey from "../src/PrivateKey.js" +import * as VKey from "../src/VKey.js" + +const SAMPLE_HASH_HEX = "9493315cd92eb5d8c4304e67b7e16ae36d61d34502694657811a2c8e" +const SAMPLE_HASH_BYTES = new Uint8Array([ + 0x94, 0x93, 0x31, 0x5c, 0xd9, 0x2e, 0xb5, 0xd8, 0xc4, 0x30, 0x4e, 0x67, 0xb7, 0xe1, 0x6a, 0xe3, 0x6d, 0x61, 0xd3, + 0x45, 0x02, 0x69, 0x46, 0x57, 0x81, 0x1a, 0x2c, 0x8e +]) + +describe("KeyHash", () => { + describe("fromHex / toHex roundtrip", () => { + it("decodes from hex and re-encodes to the same hex", () => { + const kh = KeyHash.fromHex(SAMPLE_HASH_HEX) + expect(KeyHash.toHex(kh)).toBe(SAMPLE_HASH_HEX) + }) + + it("throws on wrong-length hex", () => { + expect(() => KeyHash.fromHex("deadbeef")).toThrow() + }) + }) + + describe("fromBytes / toBytes roundtrip", () => { + it("decodes from bytes and re-encodes to identical bytes", () => { + const kh = KeyHash.fromBytes(SAMPLE_HASH_BYTES) + expect(KeyHash.toBytes(kh)).toStrictEqual(SAMPLE_HASH_BYTES) + }) + + it("hash field is 28 bytes", () => { + const kh = KeyHash.fromBytes(SAMPLE_HASH_BYTES) + expect(kh.hash.length).toBe(28) + }) + }) + + describe("fromPrivateKey", () => { + it("produces a KeyHash with 28-byte hash from a normal private key", () => { + const privKey = PrivateKey.fromBytes(PrivateKey.generate()) + const kh = KeyHash.fromPrivateKey(privKey) + expect(kh).toBeInstanceOf(KeyHash.KeyHash) + expect(kh.hash.length).toBe(28) + }) + + it("produces a KeyHash with 28-byte hash from an extended private key", () => { + const privKey = PrivateKey.fromBytes(PrivateKey.generateExtended()) + const kh = KeyHash.fromPrivateKey(privKey) + expect(kh).toBeInstanceOf(KeyHash.KeyHash) + expect(kh.hash.length).toBe(28) + }) + }) + + describe("fromVKey", () => { + it("produces the same hash as fromPrivateKey for the same key", () => { + const privKey = PrivateKey.fromBytes(PrivateKey.generate()) + const vkey = VKey.fromPrivateKey(privKey) + const fromPk = KeyHash.fromPrivateKey(privKey) + const fromVk = KeyHash.fromVKey(vkey) + expect(Bytes.equals(fromPk.hash, fromVk.hash)).toBe(true) + }) + }) + + describe("equality", () => { + it("two KeyHashes from the same hex are equal", () => { + const kh1 = KeyHash.fromHex(SAMPLE_HASH_HEX) + const kh2 = KeyHash.fromHex(SAMPLE_HASH_HEX) + expect(Equal.equals(kh1, kh2)).toBe(true) + }) + + it("two KeyHashes from different bytes are not equal", () => { + const kh1 = KeyHash.fromBytes(SAMPLE_HASH_BYTES) + const other = new Uint8Array(28).fill(0xff) + const kh2 = KeyHash.fromBytes(other) + expect(Equal.equals(kh1, kh2)).toBe(false) + }) + }) + + describe("arbitrary / property-based", () => { + it("property: fromBytes(toBytes(kh)) equals original", () => { + FastCheck.assert( + FastCheck.property(KeyHash.arbitrary, (kh) => { + const bytes = KeyHash.toBytes(kh) + const kh2 = KeyHash.fromBytes(bytes) + expect(Equal.equals(kh, kh2)).toBe(true) + }) + ) + }) + + it("property: all generated KeyHashes have 28-byte hash", () => { + FastCheck.assert( + FastCheck.property(KeyHash.arbitrary, (kh) => { + expect(kh.hash.length).toBe(28) + }) + ) + }) + }) +}) diff --git a/packages/evolution/test/ScriptHash.test.ts b/packages/evolution/test/ScriptHash.test.ts new file mode 100644 index 00000000..f5f20a0a --- /dev/null +++ b/packages/evolution/test/ScriptHash.test.ts @@ -0,0 +1,147 @@ +import * as CML from "@dcspark/cardano-multiplatform-lib-nodejs" +import { Equal, FastCheck } from "effect" +import { describe, expect, it } from "vitest" + +import * as NativeScripts from "../src/NativeScripts.js" +import * as PlutusV1 from "../src/PlutusV1.js" +import * as PlutusV2 from "../src/PlutusV2.js" +import * as PlutusV3 from "../src/PlutusV3.js" +import * as Script from "../src/Script.js" +import * as ScriptHash from "../src/ScriptHash.js" + +const SAMPLE_HASH_HEX = "b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a7" +const SAMPLE_HASH_BYTES = new Uint8Array([ + 0xb0, 0xd0, 0x7d, 0x45, 0xfe, 0x95, 0x14, 0xf8, 0x02, 0x13, 0xf4, 0x02, 0x0e, 0x5a, 0x61, 0x24, 0x14, 0x58, 0xbe, + 0x62, 0x68, 0x41, 0xcd, 0xe7, 0x17, 0xcb, 0x38, 0xa7 +]) + +const PLUTUS_BYTES = new Uint8Array([0x49, 0x48, 0x01, 0x00, 0x00, 0x22, 0x21, 0x20, 0x01, 0x01]) + +describe("ScriptHash", () => { + describe("fromHex / toHex roundtrip", () => { + it("decodes from hex and re-encodes to the same hex", () => { + const sh = ScriptHash.fromHex(SAMPLE_HASH_HEX) + expect(ScriptHash.toHex(sh)).toBe(SAMPLE_HASH_HEX) + }) + + it("throws on wrong-length hex", () => { + expect(() => ScriptHash.fromHex("deadbeef")).toThrow() + }) + }) + + describe("fromBytes / toBytes roundtrip", () => { + it("decodes from bytes and re-encodes to identical bytes", () => { + const sh = ScriptHash.fromBytes(SAMPLE_HASH_BYTES) + expect(ScriptHash.toBytes(sh)).toStrictEqual(SAMPLE_HASH_BYTES) + }) + + it("hash field is 28 bytes", () => { + const sh = ScriptHash.fromBytes(SAMPLE_HASH_BYTES) + expect(sh.hash.length).toBe(28) + }) + }) + + describe("fromScript – Plutus scripts", () => { + it("PlutusV1: produces a 28-byte hash", () => { + const script: Script.Script = new PlutusV1.PlutusV1({ bytes: PLUTUS_BYTES }) + const sh = ScriptHash.fromScript(script) + expect(sh).toBeInstanceOf(ScriptHash.ScriptHash) + expect(sh.hash.length).toBe(28) + }) + + it("PlutusV2: produces a 28-byte hash", () => { + const script: Script.Script = new PlutusV2.PlutusV2({ bytes: PLUTUS_BYTES }) + const sh = ScriptHash.fromScript(script) + expect(sh.hash.length).toBe(28) + }) + + it("PlutusV3: produces a 28-byte hash", () => { + const script: Script.Script = new PlutusV3.PlutusV3({ bytes: PLUTUS_BYTES }) + const sh = ScriptHash.fromScript(script) + expect(sh.hash.length).toBe(28) + }) + + it("PlutusV1, V2, V3 hashes differ for same bytes (different language tags)", () => { + const v1 = ScriptHash.fromScript(new PlutusV1.PlutusV1({ bytes: PLUTUS_BYTES })) + const v2 = ScriptHash.fromScript(new PlutusV2.PlutusV2({ bytes: PLUTUS_BYTES })) + const v3 = ScriptHash.fromScript(new PlutusV3.PlutusV3({ bytes: PLUTUS_BYTES })) + expect(Equal.equals(v1, v2)).toBe(false) + expect(Equal.equals(v2, v3)).toBe(false) + }) + }) + + describe("fromScript – NativeScript", () => { + it("produces a 28-byte hash", () => { + const ns = FastCheck.sample(NativeScripts.arbitrary, { seed: 1, numRuns: 1 })[0] + const script: Script.Script = ns + const sh = ScriptHash.fromScript(script) + expect(sh.hash.length).toBe(28) + }) + }) + + describe("fromScript – CML parity (Plutus)", () => { + it("PlutusV1 hash matches CML", () => { + const script: Script.Script = new PlutusV1.PlutusV1({ bytes: PLUTUS_BYTES }) + const evolutionHex = ScriptHash.toHex(ScriptHash.fromScript(script)) + + const cmlScript = CML.Script.new_plutus_v1(CML.PlutusV1Script.from_raw_bytes(PLUTUS_BYTES)) + const cmlHex = cmlScript.hash().to_hex() + + expect(evolutionHex).toBe(cmlHex) + }) + + it("PlutusV2 hash matches CML", () => { + const script: Script.Script = new PlutusV2.PlutusV2({ bytes: PLUTUS_BYTES }) + const evolutionHex = ScriptHash.toHex(ScriptHash.fromScript(script)) + + const cmlScript = CML.Script.new_plutus_v2(CML.PlutusV2Script.from_raw_bytes(PLUTUS_BYTES)) + const cmlHex = cmlScript.hash().to_hex() + + expect(evolutionHex).toBe(cmlHex) + }) + + it("PlutusV3 hash matches CML", () => { + const script: Script.Script = new PlutusV3.PlutusV3({ bytes: PLUTUS_BYTES }) + const evolutionHex = ScriptHash.toHex(ScriptHash.fromScript(script)) + + const cmlScript = CML.Script.new_plutus_v3(CML.PlutusV3Script.from_raw_bytes(PLUTUS_BYTES)) + const cmlHex = cmlScript.hash().to_hex() + + expect(evolutionHex).toBe(cmlHex) + }) + }) + + describe("equality", () => { + it("two ScriptHashes from the same hex are equal", () => { + const sh1 = ScriptHash.fromHex(SAMPLE_HASH_HEX) + const sh2 = ScriptHash.fromHex(SAMPLE_HASH_HEX) + expect(Equal.equals(sh1, sh2)).toBe(true) + }) + + it("two ScriptHashes from different bytes are not equal", () => { + const sh1 = ScriptHash.fromBytes(SAMPLE_HASH_BYTES) + const sh2 = ScriptHash.fromBytes(new Uint8Array(28).fill(0x00)) + expect(Equal.equals(sh1, sh2)).toBe(false) + }) + }) + + describe("arbitrary / property-based", () => { + it("property: fromBytes(toBytes(sh)) equals original", () => { + FastCheck.assert( + FastCheck.property(ScriptHash.arbitrary, (sh) => { + const bytes = ScriptHash.toBytes(sh) + const sh2 = ScriptHash.fromBytes(bytes) + expect(Equal.equals(sh, sh2)).toBe(true) + }) + ) + }) + + it("property: all generated ScriptHashes have 28-byte hash", () => { + FastCheck.assert( + FastCheck.property(ScriptHash.arbitrary, (sh) => { + expect(sh.hash.length).toBe(28) + }) + ) + }) + }) +})