From 2f5b4def79b1ca9f5c9fa6c493661b10da28689c Mon Sep 17 00:00:00 2001 From: solidsnakedev Date: Fri, 8 May 2026 21:24:46 -0600 Subject: [PATCH 1/3] fix(builder): skip redeemer storage for native script UTxOs in collectFrom --- .../evolution/src/sdk/builders/internal/txBuilder.ts | 1 + .../evolution/src/sdk/builders/operations/Collect.ts | 9 +++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/evolution/src/sdk/builders/internal/txBuilder.ts b/packages/evolution/src/sdk/builders/internal/txBuilder.ts index 0f9ff847..667a6769 100644 --- a/packages/evolution/src/sdk/builders/internal/txBuilder.ts +++ b/packages/evolution/src/sdk/builders/internal/txBuilder.ts @@ -618,6 +618,7 @@ export const assembleTransaction = ( } yield* Effect.logDebug(`[Assembly] WitnessSet populated:`) + yield* Effect.logDebug(` - Native scripts: ${nativeScripts.length}`) yield* Effect.logDebug(` - PlutusV1 scripts: ${plutusV1Scripts.length}`) yield* Effect.logDebug(` - PlutusV2 scripts: ${plutusV2Scripts.length}`) yield* Effect.logDebug(` - PlutusV3 scripts: ${plutusV3Scripts.length}`) diff --git a/packages/evolution/src/sdk/builders/operations/Collect.ts b/packages/evolution/src/sdk/builders/operations/Collect.ts index 2d3c4955..b75e1739 100644 --- a/packages/evolution/src/sdk/builders/operations/Collect.ts +++ b/packages/evolution/src/sdk/builders/operations/Collect.ts @@ -96,14 +96,15 @@ export const createCollectFromProgram = ( let newRedeemers = state.redeemers let newDeferredRedeemers = state.deferredRedeemers - // 5. Track redeemer information if spending from scripts - if (params.redeemer && scriptUtxos.length > 0) { + // 5. Track redeemer information if spending from Plutus scripts + // Native scripts are validated by signatures, not redeemers + if (params.redeemer && plutusScriptUtxos.length > 0) { const deferred = RedeemerBuilder.toDeferredRedeemer(params.redeemer) if (deferred._tag === "static") { // Static mode: store resolved data immediately newRedeemers = new Map(state.redeemers) - scriptUtxos.forEach((utxo) => { + plutusScriptUtxos.forEach((utxo) => { const inputKey = UTxO.toOutRefString(utxo) newRedeemers.set(inputKey, { tag: "spend", @@ -115,7 +116,7 @@ export const createCollectFromProgram = ( } else { // Self or Batch mode: store deferred for resolution after coin selection newDeferredRedeemers = new Map(state.deferredRedeemers) - scriptUtxos.forEach((utxo) => { + plutusScriptUtxos.forEach((utxo) => { const inputKey = UTxO.toOutRefString(utxo) newDeferredRedeemers.set(inputKey, { tag: "spend", From 39e1ddf628e3d275e0886453acd3553e413e0d1b Mon Sep 17 00:00:00 2001 From: solidsnakedev Date: Fri, 8 May 2026 21:24:49 -0600 Subject: [PATCH 2/3] test(builder): add native script spend redeemer regression test --- .../test/TxBuilder.NativeScriptSpend.test.ts | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 packages/evolution/test/TxBuilder.NativeScriptSpend.test.ts diff --git a/packages/evolution/test/TxBuilder.NativeScriptSpend.test.ts b/packages/evolution/test/TxBuilder.NativeScriptSpend.test.ts new file mode 100644 index 00000000..8b059bf0 --- /dev/null +++ b/packages/evolution/test/TxBuilder.NativeScriptSpend.test.ts @@ -0,0 +1,90 @@ +import { describe, expect, it } from "@effect/vitest" + +import * as CoreAddress from "../src/Address.js" +import * as CoreAssets from "../src/Assets.js" +import * as Data from "../src/Data.js" +import * as NativeScripts from "../src/NativeScripts.js" +import * as ScriptHash from "../src/ScriptHash.js" +import type { TxBuilderConfig } from "../src/sdk/builders/TransactionBuilder.js" +import { makeTxBuilder } from "../src/sdk/builders/TransactionBuilder.js" +import { mainnet } from "../src/sdk/client/index.js" +import * as CoreTransactionHash from "../src/TransactionHash.js" +import * as CoreUTxO from "../src/UTxO.js" +import { createCoreTestUtxo } from "./utils/utxo-helpers.js" + +const PROTOCOL_PARAMS = { + minFeeCoefficient: 44n, + minFeeConstant: 155_381n, + coinsPerUtxoByte: 4_310n, + maxTxSize: 16_384 +} + +const CHANGE_ADDRESS = + "addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3n0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgs68faae" + +const baseConfig: TxBuilderConfig = { chain: mainnet } + +describe("TxBuilder NativeScript Spend (#339)", () => { + it("should not create redeemers when spending from a native script address with a redeemer passed", async () => { + // Create a native script + const dummyKeyHash = new Uint8Array(28).fill(0xaa) + const nativeScript = NativeScripts.makeScriptPubKey(dummyKeyHash) + const scriptHash = ScriptHash.fromScript(nativeScript) + + // Create a script address using the native script hash + const scriptAddress = new CoreAddress.Address({ + networkId: 0, + paymentCredential: scriptHash, + stakingCredential: undefined + }) + + // Create a UTxO at the script address + const scriptUtxo = new CoreUTxO.UTxO({ + transactionId: CoreTransactionHash.fromHex("b".repeat(64)), + index: 0n, + address: scriptAddress, + assets: CoreAssets.fromLovelace(10_000_000n) + }) + + // Wallet UTxO for fees/change + const walletUtxo = createCoreTestUtxo({ + transactionId: "a".repeat(64), + index: 0n, + address: CHANGE_ADDRESS, + lovelace: 100_000_000n + }) + + // Bug reproduction: user passes a redeemer to collectFrom even though + // the UTxO is at a native script address. The builder should ignore the + // redeemer for native script UTxOs instead of creating a spend redeemer. + const signBuilder = await makeTxBuilder(baseConfig) + .attachScript({ script: nativeScript }) + .collectFrom({ + inputs: [scriptUtxo], + redeemer: new Data.Constr({ index: 0n, fields: [] }) + }) + .payToAddress({ + address: CoreAddress.fromBech32(CHANGE_ADDRESS), + assets: CoreAssets.fromLovelace(2_000_000n) + }) + .build({ + changeAddress: CoreAddress.fromBech32(CHANGE_ADDRESS), + availableUtxos: [walletUtxo], + protocolParameters: PROTOCOL_PARAMS + }) + + const tx = await signBuilder.toTransaction() + + // The native script must be in the witness set + expect(tx.witnessSet.nativeScripts.length).toBeGreaterThan(0) + + // There must be NO redeemers (native scripts use signatures, not redeemers) + expect(tx.witnessSet.redeemers).toBeUndefined() + + // There must be NO scriptDataHash (only needed for Plutus scripts) + expect(tx.body.scriptDataHash).toBeUndefined() + + // There must be NO collateral (only needed for Plutus scripts) + expect(tx.body.collateralInputs).toBeUndefined() + }) +}) From 11754608bec389b2f3743787c2b2040636b8e36f Mon Sep 17 00:00:00 2001 From: solidsnakedev Date: Fri, 8 May 2026 21:24:52 -0600 Subject: [PATCH 3/3] release: changeset for native script spend fix --- .changeset/fix-native-script-spend-redeemer.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fix-native-script-spend-redeemer.md diff --git a/.changeset/fix-native-script-spend-redeemer.md b/.changeset/fix-native-script-spend-redeemer.md new file mode 100644 index 00000000..cfd215de --- /dev/null +++ b/.changeset/fix-native-script-spend-redeemer.md @@ -0,0 +1,5 @@ +--- +"@evolution-sdk/evolution": patch +--- + +Fix collectFrom storing redeemers for native script UTxOs. When a redeemer was passed to collectFrom for a native script input, it was incorrectly treated as a Plutus spend, causing "associated script witness is missing" errors during evaluation.