diff --git a/.changeset/native-script-drep-registration.md b/.changeset/native-script-drep-registration.md new file mode 100644 index 00000000..f1a45030 --- /dev/null +++ b/.changeset/native-script-drep-registration.md @@ -0,0 +1,5 @@ +--- +"@evolution-sdk/evolution": patch +--- + +`TxBuilder` no longer requires a Plutus redeemer to register, update, or deregister a native-script (multisig) DRep. A script-controlled DRep certificate whose script is native is authorized by vkey witnesses, not a redeemer, so previously these certificates could not be built even though they are ledger-valid. Mirroring the native-script vote fix, the native-vs-Plutus distinction is now made at build time, once the DRep's script is attached via `.attachScript()` (or supplied through a reference input): a redeemer is required only for Plutus-script DRep credentials, and a redeemer mistakenly supplied for a native-script DRep certificate is pruned from the transaction. \ No newline at end of file diff --git a/packages/evolution-devnet/test/TxBuilder.MixedVoters.test.ts b/packages/evolution-devnet/test/TxBuilder.MixedVoters.test.ts new file mode 100644 index 00000000..8c9b9d60 --- /dev/null +++ b/packages/evolution-devnet/test/TxBuilder.MixedVoters.test.ts @@ -0,0 +1,243 @@ +/** + * Devnet test for a single governance vote transaction carrying BOTH a Plutus-script + * DRep voter and a native-script (multisig) DRep voter. + * + * This exercises the build-time native-vs-Plutus classification end-to-end: + * - the Plutus voter keeps its redeemer, + * - the native voter's redeemer is pruned (it is satisfied by vkey witnesses), + * - and the ledger accepts the mixed transaction. + */ + +import { afterAll, beforeAll, describe, expect, it } from "@effect/vitest" +import * as Cluster from "@evolution-sdk/devnet/Cluster" +import * as Config from "@evolution-sdk/devnet/Config" +import * as Genesis from "@evolution-sdk/devnet/Genesis" +import type { Cardano } from "@evolution-sdk/evolution" +import { Client, preprod } from "@evolution-sdk/evolution" +import * as Address from "@evolution-sdk/evolution/Address" +import * as Anchor from "@evolution-sdk/evolution/Anchor" +import * as Bytes from "@evolution-sdk/evolution/Bytes" +import * as Bytes32 from "@evolution-sdk/evolution/Bytes32" +import * as Data from "@evolution-sdk/evolution/Data" +import * as DRep from "@evolution-sdk/evolution/DRep" +import * as GovernanceAction from "@evolution-sdk/evolution/GovernanceAction" +import * as NativeScripts from "@evolution-sdk/evolution/NativeScripts" +import * as PlutusV3 from "@evolution-sdk/evolution/PlutusV3" +import * as RewardAccount from "@evolution-sdk/evolution/RewardAccount" +import * as ScriptHash from "@evolution-sdk/evolution/ScriptHash" +import * as Url from "@evolution-sdk/evolution/Url" +import * as VotingProcedures from "@evolution-sdk/evolution/VotingProcedures" + +import plutusJson from "../../evolution/test/spec/plutus.json" + +const TEST_MNEMONIC = + "test test test test test test test test test test test test test test test test test test test test test test test sauce" + +const loadValidator = (title: string) => { + const validator = plutusJson.validators.find((v: any) => v.title === title) + if (!validator) throw new Error(`${title} validator not found`) + return validator.compiledCode +} + +const makeAnchor = (url: string) => + new Anchor.Anchor({ + anchorUrl: new Url.Url({ href: url }), + anchorDataHash: Bytes32.fromHex("0".repeat(64)) + }) + +describe("TxBuilder Mixed Voters (Plutus + native DRep in one vote tx)", () => { + let devnetCluster: Cluster.Cluster | undefined + + const createTestClient = (accountIndex: number = 0) => { + if (!devnetCluster) throw new Error("Cluster not initialized") + return Client.make(Cluster.getChain(devnetCluster)) + .withKupmios({ + kupoUrl: `http://localhost:${devnetCluster!.ports.kupo}`, + ogmiosUrl: `http://localhost:${devnetCluster!.ports.ogmios}` + }) + .withSeed({ mnemonic: TEST_MNEMONIC, accountIndex, addressType: "Base" }) + } + const genesisUtxosByAccount: Map = new Map() + + beforeAll(async () => { + const accounts = [0, 1].map((accountIndex) => + Client.make(preprod).withSeed({ mnemonic: TEST_MNEMONIC, accountIndex, addressType: "Base" }) + ) + const addresses = await Promise.all(accounts.map((client) => client.address())) + + const genesisConfig: Config.ShelleyGenesis = { + ...Config.DEFAULT_SHELLEY_GENESIS, + slotLength: 0.02, + epochLength: 50, + activeSlotsCoeff: 1.0, + initialFunds: Object.fromEntries(addresses.map((addr) => [Address.toHex(addr), 500_000_000_000])) + } + + const genesisUtxos = await Genesis.calculateUtxosFromConfig(genesisConfig) + addresses.forEach((addr, i) => { + const utxo = genesisUtxos.find((u) => Address.toBech32(u.address) === Address.toBech32(addr)) + if (utxo) genesisUtxosByAccount.set(i, utxo) + }) + + devnetCluster = await Cluster.make({ + clusterName: "mixed-voters-test", + shelleyGenesis: genesisConfig, + conwayGenesis: { ...Config.DEFAULT_CONWAY_GENESIS, govActionLifetime: 30 }, + kupo: { enabled: true, logLevel: "Info" }, + ogmios: { enabled: true, logLevel: "info" } + }) + + await Cluster.start(devnetCluster) + await new Promise((r) => setTimeout(r, 3_000)) + }, 180_000) + + afterAll(async () => { + if (devnetCluster) { + await Cluster.stop(devnetCluster) + await Cluster.remove(devnetCluster) + } + }, 60_000) + + it("casts a vote from a Plutus DRep and a native-script DRep in one transaction", { timeout: 180_000 }, async () => { + const client0 = createTestClient(0) + const client1 = createTestClient(1) + const address0 = await client0.address() + const address1 = await client1.address() + if (!address0.stakingCredential) throw new Error("Need staking credential") + + const pkh0 = address0.paymentCredential + const pkh1 = address1.paymentCredential + if (pkh0._tag !== "KeyHash" || pkh1._tag !== "KeyHash") { + throw new Error("Need key hash credentials") + } + + // Plutus-script DRep (always_yes_drep: publish + vote always succeed) + const plutusScript = new PlutusV3.PlutusV3({ + bytes: Bytes.fromHex(loadValidator("governance_voting.always_yes_drep.publish")) + }) + const plutusScriptHash = ScriptHash.fromScript(plutusScript) + const plutusDRepCredential = new ScriptHash.ScriptHash({ hash: plutusScriptHash.hash }) + const plutusRedeemer = Data.constr(0n, [Data.int(1n)]) + + // Native-script DRep (2-of-2 multisig of pkh0 + pkh1) + const nativeScript = NativeScripts.makeScriptNOfK(2n, [ + NativeScripts.makeScriptPubKey(pkh0.hash).script, + NativeScripts.makeScriptPubKey(pkh1.hash).script + ]) + const nativeScriptHash = ScriptHash.fromScript(nativeScript) + const nativeDRepCredential = new ScriptHash.ScriptHash({ hash: nativeScriptHash.hash }) + + // Register stake (required for proposal submission). + const stakeRegTx = await client0 + .newTx() + .registerStake({ stakeCredential: address0.stakingCredential }) + .build({ availableUtxos: [genesisUtxosByAccount.get(0)!] }) + .then((b) => b.sign()) + .then((b) => b.submit()) + expect(await client0.awaitTx(stakeRegTx, 1000)).toBe(true) + + await new Promise((r) => setTimeout(r, 2_000)) + + // Register the Plutus-script DRep (redeemer required). + const plutusDRepRegTx = await client0 + .newTx() + .registerDRep({ + drepCredential: plutusDRepCredential, + anchor: makeAnchor("https://example.com/plutus-drep.json"), + redeemer: plutusRedeemer + }) + .attachScript({ script: plutusScript }) + .build() + .then((b) => b.sign()) + .then((b) => b.submit()) + expect(await client0.awaitTx(plutusDRepRegTx, 1000)).toBe(true) + + await new Promise((r) => setTimeout(r, 2_000)) + + // Register the native-script DRep (no redeemer; authorized by the 2-of-2 signatures). + const nativeRegSignBuilder = await client0 + .newTx() + .registerDRep({ + drepCredential: nativeDRepCredential, + anchor: makeAnchor("https://example.com/native-drep.json") + }) + .attachScript({ script: nativeScript }) + .build() + + // The native registration tx must NOT carry redeemers or a script data hash. + const nativeRegTx = await nativeRegSignBuilder.toTransaction() + expect(nativeRegTx.witnessSet.redeemers).toBeUndefined() + expect(nativeRegTx.body.scriptDataHash).toBeUndefined() + expect(nativeRegTx.witnessSet.nativeScripts?.length ?? 0).toBeGreaterThan(0) + + const nativeRegWitness0 = await nativeRegSignBuilder.partialSign() + const nativeRegWitness1 = await client1.signTx(nativeRegTx) + const nativeRegTxHash = await nativeRegSignBuilder + .assemble([nativeRegWitness0, nativeRegWitness1]) + .then((b) => b.submit()) + expect(await client0.awaitTx(nativeRegTxHash, 1000)).toBe(true) + + await new Promise((r) => setTimeout(r, 2_000)) + + // Create a proposal to vote on. + const rewardAccount = new RewardAccount.RewardAccount({ + networkId: 0, + stakeCredential: address0.stakingCredential + }) + const proposeTx = await client0 + .newTx() + .propose({ + governanceAction: new GovernanceAction.InfoAction({}), + rewardAccount, + anchor: makeAnchor("https://example.com/proposal.json") + }) + .build() + .then((b) => b.sign()) + .then((b) => b.submit()) + expect(await client0.awaitTx(proposeTx, 1000)).toBe(true) + + await new Promise((r) => setTimeout(r, 2_000)) + + // --- The mixed vote: BOTH DReps vote yes on the same governance action --- + const govActionId = new GovernanceAction.GovActionId({ + transactionId: proposeTx, + govActionIndex: 0n + }) + const yesProcedure = new VotingProcedures.VotingProcedure({ vote: VotingProcedures.yes(), anchor: null }) + + const votingProcedures = new VotingProcedures.VotingProcedures({ + procedures: new Map([ + [ + new VotingProcedures.DRepVoter({ drep: DRep.fromScriptHash(plutusScriptHash) }), + new Map([[govActionId, yesProcedure]]) + ], + [ + new VotingProcedures.DRepVoter({ drep: DRep.fromScriptHash(nativeScriptHash) }), + new Map([[govActionId, yesProcedure]]) + ] + ]) + }) + + // The redeemer is applied to all script voters; the native voter's copy is pruned at + // build time, leaving exactly one redeemer for the Plutus voter. + const voteSignBuilder = await client0 + .newTx() + .vote({ votingProcedures, redeemer: plutusRedeemer }) + .attachScript({ script: plutusScript }) + .attachScript({ script: nativeScript }) + .build() + + const voteTx = await voteSignBuilder.toTransaction() + + expect(voteTx.body.scriptDataHash).toBeDefined() + expect(voteTx.witnessSet.plutusV3Scripts?.length ?? 0).toBeGreaterThan(0) + expect(voteTx.witnessSet.nativeScripts?.length ?? 0).toBeGreaterThan(0) + + const voteWitness0 = await voteSignBuilder.partialSign() + const voteWitness1 = await client1.signTx(voteTx) + const submitBuilder = await voteSignBuilder.assemble([voteWitness0, voteWitness1]) + const voteTxHash = await submitBuilder.submit() + + expect(await client0.awaitTx(voteTxHash, 1000)).toBe(true) + }) +}) diff --git a/packages/evolution/src/sdk/builders/internal/build.ts b/packages/evolution/src/sdk/builders/internal/build.ts index 41356062..43e32bfa 100644 --- a/packages/evolution/src/sdk/builders/internal/build.ts +++ b/packages/evolution/src/sdk/builders/internal/build.ts @@ -167,6 +167,7 @@ export const makeBuild = ( yield* Effect.all(programs, { concurrency: "unbounded" }) yield* TxBuilderImpl.validateVoterRedeemers + yield* TxBuilderImpl.validateCertRedeemers const { transaction, txWithFakeWitnesses } = yield* phaseStateMachine diff --git a/packages/evolution/src/sdk/builders/internal/txBuilder.ts b/packages/evolution/src/sdk/builders/internal/txBuilder.ts index 895b6878..90ea8c12 100644 --- a/packages/evolution/src/sdk/builders/internal/txBuilder.ts +++ b/packages/evolution/src/sdk/builders/internal/txBuilder.ts @@ -281,6 +281,42 @@ const voterScriptHashHex = (voter: VotingProcedures.Voter): string | undefined = } } +/** + * Resolve the DRep credential hash (as hex) for the DRep lifecycle certificates when controlled by a script, or `undefined` + * for key-hash credentials and non-DRep certificates. + */ +const drepCertScriptHashHex = (certificate: Certificate.Certificate): string | undefined => { + switch (certificate._tag) { + case "RegDrepCert": + case "UpdateDrepCert": + case "UnregDrepCert": + return certificate.drepCredential._tag === "ScriptHash" + ? Bytes.toHex(certificate.drepCredential.hash) + : undefined + default: + return undefined + } +} + +/** + * Build a resolver that reports whether the script behind a given hash is a native + * script. Looks at scripts attached via `.attachScript()` first, then any reference + * inputs carrying a script. Returns `undefined` when the script cannot be classified. + */ +const makeIsNativeScript = + (state: { scripts: ReadonlyMap; referenceInputs: ReadonlyArray }) => + (scriptHashHex: string): boolean | undefined => { + const attached = state.scripts.get(scriptHashHex) + if (attached) return attached._tag === "NativeScript" + + for (const ref of state.referenceInputs) { + if (ref.scriptRef && ScriptHash.toHex(ScriptHash.fromScript(ref.scriptRef)) === scriptHashHex) { + return ref.scriptRef._tag === "NativeScript" + } + } + return undefined + } + /** * Validate redeemer requirements for governance voters, distinguishing native-script * voters (satisfied by vkey witnesses) from Plutus-script voters (which require a redeemer). @@ -298,17 +334,7 @@ export const validateVoterRedeemers: Effect.Effect { - const attached = state.scripts.get(scriptHashHex) - if (attached) return attached._tag === "NativeScript" - - for (const ref of state.referenceInputs) { - if (ref.scriptRef && ScriptHash.toHex(ScriptHash.fromScript(ref.scriptRef)) === scriptHashHex) { - return ref.scriptRef._tag === "NativeScript" - } - } - return undefined - } + const isNativeScript = makeIsNativeScript(state) const votersMissingRedeemer: Array = [] const nativeVoterRedeemerKeys: Array = [] @@ -360,6 +386,75 @@ export const validateVoterRedeemers: Effect.Effect = Effect.gen( + function* () { + const stateRef = yield* TxContext + const state = yield* Ref.get(stateRef) + + if (state.certificates.length === 0) { + return + } + + const isNativeScript = makeIsNativeScript(state) + + const certsMissingRedeemer: Array = [] + const nativeCertRedeemerKeys: Array = [] + + for (const certificate of state.certificates) { + const scriptHashHex = drepCertScriptHashHex(certificate) + if (scriptHashHex === undefined) continue + + const certKey = `cert:${scriptHashHex}` + const hasRedeemer = state.redeemers.has(certKey) || state.deferredRedeemers.has(certKey) + const native = isNativeScript(scriptHashHex) + + if (native === true) { + if (hasRedeemer) nativeCertRedeemerKeys.push(certKey) + } else { + if (!hasRedeemer) certsMissingRedeemer.push(certKey) + } + } + + if (certsMissingRedeemer.length > 0) { + return yield* Effect.fail( + new TransactionBuilderError({ + message: + `Redeemer required for ${certsMissingRedeemer.length} non-native-script DRep certificate(s): ` + + `${certsMissingRedeemer.join(", ")}. ` + + `If the DRep is a native (multisig) script, attach it via .attachScript() ` + + `(or provide it through a reference input) so it is recognized and no redeemer is needed; ` + + `if it is a Plutus script, supply a redeemer.`, + cause: certsMissingRedeemer + }) + ) + } + + if (nativeCertRedeemerKeys.length > 0) { + yield* Effect.logDebug( + `[Cert] Ignoring redeemer(s) supplied for native-script DRep certificate(s): ${nativeCertRedeemerKeys.join(", ")}. ` + + `Native scripts are satisfied by vkey witnesses, not redeemers.` + ) + yield* Ref.update(stateRef, (s) => { + const redeemers = new Map(s.redeemers) + const deferredRedeemers = new Map(s.deferredRedeemers) + for (const key of nativeCertRedeemerKeys) { + redeemers.delete(key) + deferredRedeemers.delete(key) + } + return { ...s, redeemers, deferredRedeemers } + }) + } + } +) + /** * Assemble a Transaction from inputs, outputs, and calculated fee. * Creates TransactionBody with all required fields. diff --git a/packages/evolution/src/sdk/builders/operations/Governance.ts b/packages/evolution/src/sdk/builders/operations/Governance.ts index f3346a16..323279af 100644 --- a/packages/evolution/src/sdk/builders/operations/Governance.ts +++ b/packages/evolution/src/sdk/builders/operations/Governance.ts @@ -40,14 +40,6 @@ export const createRegisterDRepProgram = ( const isScriptControlled = params.drepCredential._tag === "ScriptHash" - if (isScriptControlled && !params.redeemer) { - return yield* Effect.fail( - new TransactionBuilderError({ - message: "Redeemer required for script-controlled DRep credential registration" - }) - ) - } - if (!fullParams) { return yield* Effect.fail( new TransactionBuilderError({ message: "Provider required to fetch protocol parameters for DRep registration" }) @@ -115,18 +107,8 @@ export const createUpdateDRepProgram = ( ): Effect.Effect => Effect.gen(function* () { const ctx = yield* TxContext - - // Check if script-controlled const isScriptControlled = params.drepCredential._tag === "ScriptHash" - if (isScriptControlled && !params.redeemer) { - return yield* Effect.fail( - new TransactionBuilderError({ - message: "Redeemer required for script-controlled DRep credential update" - }) - ) - } - // Create UpdateDrepCert certificate const certificate = new Certificate.UpdateDrepCert({ drepCredential: params.drepCredential, @@ -188,14 +170,6 @@ export const createDeregisterDRepProgram = ( const isScriptControlled = params.drepCredential._tag === "ScriptHash" - if (isScriptControlled && !params.redeemer) { - return yield* Effect.fail( - new TransactionBuilderError({ - message: "Redeemer required for script-controlled DRep credential deregistration" - }) - ) - } - if (!fullParams) { return yield* Effect.fail( new TransactionBuilderError({ message: "Provider required to fetch protocol parameters for DRep deregistration" }) diff --git a/packages/evolution/test/TxBuilder.NativeScriptDRepCert.test.ts b/packages/evolution/test/TxBuilder.NativeScriptDRepCert.test.ts new file mode 100644 index 00000000..2a469f63 --- /dev/null +++ b/packages/evolution/test/TxBuilder.NativeScriptDRepCert.test.ts @@ -0,0 +1,153 @@ +import { describe, expect, it } from "@effect/vitest" + +import * as CoreAddress from "../src/Address.js" +import * as Data from "../src/Data.js" +import * as NativeScripts from "../src/NativeScripts.js" +import * as ScriptHash from "../src/ScriptHash.js" +import { makeTxBuilder } from "../src/sdk/builders/TransactionBuilder.js" +import { mainnet } from "../src/sdk/client/index.js" +import type { ProtocolParameters } from "../src/sdk/provider/Provider.js" +import type * as CoreUTxO from "../src/UTxO.js" +import { createCoreTestUtxo } from "./utils/utxo-helpers.js" + +const CHANGE_ADDRESS = + "addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3n0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgs68faae" + +const FULL_PROTOCOL_PARAMS = { + minFeeA: 44, + minFeeB: 155_381, + maxTxSize: 16_384, + maxValSize: 5_000, + keyDeposit: 2_000_000n, + poolDeposit: 500_000_000n, + drepDeposit: 500_000_000n, + govActionDeposit: 100_000_000_000n, + priceMem: 0.0577, + priceStep: 0.0000721, + maxTxExMem: 14_000_000n, + maxTxExSteps: 10_000_000_000n, + coinsPerUtxoByte: 4_310n, + collateralPercentage: 150, + maxCollateralInputs: 3, + minFeeRefScriptCostPerByte: 15, + costModels: { + PlutusV1: {} as Record, + PlutusV2: {} as Record, + PlutusV3: {} as Record + } +} satisfies ProtocolParameters + +const baseConfig = { chain: mainnet } + +const makeFundedUtxos = (lovelace: bigint): Array => [ + createCoreTestUtxo({ + transactionId: "a".repeat(64), + index: 0n, + address: CHANGE_ADDRESS, + lovelace + }) +] + +// A 2-of-3 native (multisig) script and the DRep credential it controls. +const makeMultisigDRep = () => { + const keyHashes = [0xaa, 0xbb, 0xcc].map((b) => new Uint8Array(28).fill(b)) + const script = NativeScripts.makeScriptNOfK( + 2n, + keyHashes.map((kh) => NativeScripts.makeScriptPubKey(kh).script) + ) + const scriptHash = ScriptHash.fromScript(script) + const drepCredential = new ScriptHash.ScriptHash({ hash: scriptHash.hash }) + return { script, scriptHash, drepCredential } +} + +describe("TxBuilder NativeScript DRep certificate (native-script DRep credential)", () => { + it("registers a native-script DRep with no redeemer", async () => { + const { drepCredential, script } = makeMultisigDRep() + + const signBuilder = await makeTxBuilder(baseConfig) + .registerDRep({ drepCredential }) + .attachScript({ script }) + .build({ + changeAddress: CoreAddress.fromBech32(CHANGE_ADDRESS), + availableUtxos: makeFundedUtxos(503_000_000n), + fullProtocolParameters: FULL_PROTOCOL_PARAMS + }) + + const tx = await signBuilder.toTransaction() + + expect(tx.witnessSet.nativeScripts?.length ?? 0).toBeGreaterThan(0) + expect(tx.witnessSet.redeemers).toBeUndefined() + expect(tx.body.scriptDataHash).toBeUndefined() + expect(tx.body.certificates?.length ?? 0).toBeGreaterThan(0) + }) + + it("classifies the native DRep when .attachScript() is called before .registerDRep() (order-independent)", async () => { + const { drepCredential, script } = makeMultisigDRep() + + const signBuilder = await makeTxBuilder(baseConfig) + .attachScript({ script }) + .registerDRep({ drepCredential }) + .build({ + changeAddress: CoreAddress.fromBech32(CHANGE_ADDRESS), + availableUtxos: makeFundedUtxos(503_000_000n), + fullProtocolParameters: FULL_PROTOCOL_PARAMS + }) + + const tx = await signBuilder.toTransaction() + expect(tx.witnessSet.nativeScripts?.length ?? 0).toBeGreaterThan(0) + expect(tx.witnessSet.redeemers).toBeUndefined() + }) + + it("ignores a redeemer supplied for a native-script DRep registration (no redeemer emitted)", async () => { + const { drepCredential, script } = makeMultisigDRep() + + const signBuilder = await makeTxBuilder(baseConfig) + .registerDRep({ + drepCredential, + redeemer: new Data.Constr({ index: 0n, fields: [] }) + }) + .attachScript({ script }) + .build({ + changeAddress: CoreAddress.fromBech32(CHANGE_ADDRESS), + availableUtxos: makeFundedUtxos(503_000_000n), + fullProtocolParameters: FULL_PROTOCOL_PARAMS + }) + + const tx = await signBuilder.toTransaction() + expect(tx.witnessSet.nativeScripts?.length ?? 0).toBeGreaterThan(0) + expect(tx.witnessSet.redeemers).toBeUndefined() + expect(tx.body.scriptDataHash).toBeUndefined() + }) + + it("deregisters a native-script DRep with no redeemer", async () => { + const { drepCredential, script } = makeMultisigDRep() + + const signBuilder = await makeTxBuilder(baseConfig) + .deregisterDRep({ drepCredential }) + .attachScript({ script }) + .build({ + changeAddress: CoreAddress.fromBech32(CHANGE_ADDRESS), + availableUtxos: makeFundedUtxos(2_000_000n), + fullProtocolParameters: FULL_PROTOCOL_PARAMS + }) + + const tx = await signBuilder.toTransaction() + expect(tx.witnessSet.nativeScripts?.length ?? 0).toBeGreaterThan(0) + expect(tx.witnessSet.redeemers).toBeUndefined() + }) + + it("still rejects a Plutus-script DRep registration when no redeemer is provided", async () => { + const plutusScriptHash = ScriptHash.fromHex("11".repeat(28)) + const drepCredential = new ScriptHash.ScriptHash({ hash: plutusScriptHash.hash }) + + await expect( + makeTxBuilder(baseConfig) + .registerDRep({ drepCredential }) + .build({ + changeAddress: CoreAddress.fromBech32(CHANGE_ADDRESS), + availableUtxos: makeFundedUtxos(503_000_000n), + fullProtocolParameters: FULL_PROTOCOL_PARAMS + }) + ).rejects.toThrow(/[Rr]edeemer required/) + }) +})