From 99ce46ceb3feb8739771a357d5b0cf495bf6c272 Mon Sep 17 00:00:00 2001 From: Kyle Cox Date: Wed, 14 Oct 2026 18:54:44 +0000 Subject: [PATCH] fix(evolution): encode a JS boolean Bool constant as its own value (#398) ConstantValue permits a plain JS boolean, but the flat encoder's Bool branch read .index off the value, so con bool false encoded with the True bit and produced the same flat bytes as con bool true. Handle the boolean form in encodeData; the decoded { index } form is unchanged, and booleans nested in a list of Bool go through the same branch. Tests: 4 new in UPLC.test.ts (3 RED on main, 1 control). UPLC file 43/43; full suite 1682 passed / 75 skipped / 0 failed. --- .changeset/uplc-bool-js-boolean.md | 5 ++++ packages/evolution/src/UPLC.ts | 7 +++-- packages/evolution/test/UPLC.test.ts | 45 ++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 .changeset/uplc-bool-js-boolean.md diff --git a/.changeset/uplc-bool-js-boolean.md b/.changeset/uplc-bool-js-boolean.md new file mode 100644 index 00000000..0f828324 --- /dev/null +++ b/.changeset/uplc-bool-js-boolean.md @@ -0,0 +1,5 @@ +--- +"@evolution-sdk/evolution": patch +--- + +Fix the UPLC flat encoder encoding a Bool constant given as a plain JS boolean `false` as true. `ConstantValue` permits a JS boolean, but the Bool encoder read `.index` off the value, which a boolean does not have, so `con bool false` and `con bool true` produced identical flat bytes. The encoder now handles the boolean form directly; the decoded `{ index }` form encodes exactly as before. diff --git a/packages/evolution/src/UPLC.ts b/packages/evolution/src/UPLC.ts index 760f9ef8..ef8d89d3 100644 --- a/packages/evolution/src/UPLC.ts +++ b/packages/evolution/src/UPLC.ts @@ -950,8 +950,11 @@ const encodeData = (state: EncodeState, dataType: DataType, value: ConstantValue } else if (dataType === "Unit") { // No encoding needed } else if (dataType === "Bool") { - const boolVal = value as { index: bigint } - pushBit(state, boolVal.index === 0n ? 0 : 1) + // ConstantValue permits a plain JS boolean as well as the decoded + // { index } form; a boolean has no .index, so reading it would + // encode false as true. + const bit = typeof value === "boolean" ? (value ? 1 : 0) : (value as { index: bigint }).index === 0n ? 0 : 1 + pushBit(state, bit) } else if (dataType === "Data") { encodeByteString(state, value as Uint8Array) } else if ("list" in dataType) { diff --git a/packages/evolution/test/UPLC.test.ts b/packages/evolution/test/UPLC.test.ts index 349ea76f..720473cb 100644 --- a/packages/evolution/test/UPLC.test.ts +++ b/packages/evolution/test/UPLC.test.ts @@ -167,6 +167,51 @@ describe("UPLC Module", () => { const decoded2 = UPLC.fromFlatBytes(reencoded) expect(decoded2.version).toBe(program.version) }) + + describe("Bool constants given as plain JS booleans", () => { + const boolProgram = (value: UPLC.ConstantValue) => + new UPLC.Program({ version: "1.0.0", body: UPLC.constantTerm("Bool", value) }) + + const decodedConstant = (bytes: Uint8Array): UPLC.ConstantValue => { + const decoded = UPLC.fromFlatBytes(bytes) + if (decoded.body.type !== "Constant") throw new Error("expected a Constant term") + return decoded.body.value + } + + it("encodes JS false as False, not True", () => { + const falseBytes = UPLC.toFlatBytes(boolProgram(false)) + const trueBytes = UPLC.toFlatBytes(boolProgram(true)) + expect(Bytes.toHex(falseBytes)).not.toBe(Bytes.toHex(trueBytes)) + expect(decodedConstant(falseBytes)).toEqual({ index: 0n, fields: [] }) + }) + + it("encodes JS true as True", () => { + const trueBytes = UPLC.toFlatBytes(boolProgram(true)) + expect(decodedConstant(trueBytes)).toEqual({ index: 1n, fields: [] }) + }) + + it("matches the { index } representation byte for byte", () => { + expect(Bytes.toHex(UPLC.toFlatBytes(boolProgram(false)))).toBe( + Bytes.toHex(UPLC.toFlatBytes(boolProgram({ index: 0n, fields: [] }))) + ) + expect(Bytes.toHex(UPLC.toFlatBytes(boolProgram(true)))).toBe( + Bytes.toHex(UPLC.toFlatBytes(boolProgram({ index: 1n, fields: [] }))) + ) + }) + + it("roundtrips JS booleans nested in a list of Bool", () => { + const program = new UPLC.Program({ + version: "1.0.0", + body: UPLC.constantTerm({ list: "Bool" }, { items: [false, true] }) + }) + expect(decodedConstant(UPLC.toFlatBytes(program))).toEqual({ + items: [ + { index: 0n, fields: [] }, + { index: 1n, fields: [] } + ] + }) + }) + }) }) describe("Schema Transform - FromFlatBytes", () => {