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
69 changes: 61 additions & 8 deletions packages/evolution/src/sdk/builders/internal/txBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1211,9 +1211,6 @@ export const buildFakeWitnessSet = (
switch (script._tag) {
case "NativeScript": {
nativeScripts.push(script)
// Count required signers for this native script and add fake witnesses
const requiredSigners = addNativeScriptWitnesses(script)
yield* Effect.logDebug(`[buildFakeWitnessSet] Native script requires ${requiredSigners} signers`)
break
}
case "PlutusV1":
Expand All @@ -1228,11 +1225,67 @@ export const buildFakeWitnessSet = (
}
}

// Also count required signers from scripts carried by reference or spent inputs
for (const utxo of [...state.referenceInputs, ...inputUtxos]) {
if (utxo.scriptRef && utxo.scriptRef._tag === "NativeScript") {
const requiredSigners = addNativeScriptWitnesses(utxo.scriptRef)
yield* Effect.logDebug(`[buildFakeWitnessSet] Input-carried native script requires ${requiredSigners} signers`)
// Count native-script signers only for scripts required by an actual transaction purpose.
const requiredScriptHashes = new Set<string>()
for (const utxo of state.selectedUtxos) {
if (utxo.address.paymentCredential._tag === "ScriptHash") {
requiredScriptHashes.add(ScriptHash.toHex(utxo.address.paymentCredential))
}
}
if (state.mint) {
for (const policyId of state.mint.map.keys()) {
requiredScriptHashes.add(PolicyId.toHex(policyId))
}
}
for (const certificate of state.certificates) {
let scriptHashHex = certScriptHashHex(certificate)
if (scriptHashHex === undefined) {
switch (certificate._tag) {
case "StakeRegistration":
case "StakeDeregistration":
case "StakeDelegation":
case "RegCert":
case "UnregCert":
case "VoteDelegCert":
case "StakeVoteDelegCert":
case "StakeRegDelegCert":
case "VoteRegDelegCert":
case "StakeVoteRegDelegCert":
if (certificate.stakeCredential._tag === "ScriptHash") {
scriptHashHex = ScriptHash.toHex(certificate.stakeCredential)
}
break
}
}
if (scriptHashHex !== undefined) requiredScriptHashes.add(scriptHashHex)
}
for (const [rewardAccount] of state.withdrawals) {
if (rewardAccount.stakeCredential._tag === "ScriptHash") {
requiredScriptHashes.add(ScriptHash.toHex(rewardAccount.stakeCredential))
}
}
for (const voter of state.votingProcedures?.procedures.keys() ?? []) {
const scriptHashHex = voterScriptHashHex(voter)
if (scriptHashHex !== undefined) requiredScriptHashes.add(scriptHashHex)
}
for (const procedure of state.proposalProcedures?.procedures ?? []) {
const scriptHashHex = proposalPolicyHashHex(procedure.governanceAction)
if (scriptHashHex !== undefined) requiredScriptHashes.add(scriptHashHex)
}

const isNativeScript = makeIsNativeScript(state)
for (const scriptHashHex of requiredScriptHashes) {
if (isNativeScript(scriptHashHex) !== true) continue

const script = state.scripts.get(scriptHashHex) ??
[...state.referenceInputs, ...state.selectedUtxos]
.map((utxo) => utxo.scriptRef)
.find((script) =>
script?._tag === "NativeScript" && ScriptHash.toHex(ScriptHash.fromScript(script)) === scriptHashHex
)
if (script?._tag === "NativeScript") {
const requiredSigners = addNativeScriptWitnesses(script)
yield* Effect.logDebug(`[buildFakeWitnessSet] Required native script requires ${requiredSigners} signers`)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { describe, expect, it } from "@effect/vitest"
import { Effect, Ref } from "effect"

import * as CoreAddress from "../src/Address.js"
import * as CoreAssets from "../src/Assets.js"
import * as Certificate from "../src/Certificate.js"
import * as NativeScripts from "../src/NativeScripts.js"
import * as ScriptHash from "../src/ScriptHash.js"
import * as BuilderState from "../src/sdk/builders/internal/state.js"
import { buildFakeWitnessSet } from "../src/sdk/builders/internal/txBuilder.js"
import { makeTxBuilder, type TxBuilderConfig, type TxBuilderState,TxContext } from "../src/sdk/builders/TransactionBuilder.js"
import { mainnet } from "../src/sdk/client/index.js"
import * as TransactionHash 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 = { chain: mainnet } satisfies TxBuilderConfig

const unusedNativeScript = NativeScripts.makeScriptPubKey(new Uint8Array(28).fill(0xcc))

const scriptCarrier = (hash: string, scriptRef = unusedNativeScript) =>
new CoreUTxO.UTxO({
transactionId: TransactionHash.fromHex(hash),
index: 0n,
address: CoreAddress.fromBech32(CHANGE_ADDRESS),
assets: CoreAssets.fromLovelace(20_000_000n),
scriptRef
})

const buildOptions = {
changeAddress: CoreAddress.fromBech32(CHANGE_ADDRESS),
availableUtxos: [
createCoreTestUtxo({
transactionId: "a".repeat(64),
index: 0,
address: CHANGE_ADDRESS,
lovelace: 100_000_000n
})
],
protocolParameters: PROTOCOL_PARAMS
}

describe("TxBuilder native script signer estimation (#596)", () => {
it("does not count signers for an unrelated native script carried by a spent input", async () => {
const builder = await makeTxBuilder(baseConfig)
.collectFrom({ inputs: [scriptCarrier("b".repeat(64))] })
.build(buildOptions)

const fakeTx = await builder.toTransactionWithFakeWitnesses()
expect(fakeTx.witnessSet.vkeyWitnesses?.length ?? 0).toBe(1)
})

it("does not count signers for an unrelated native script carried by a reference input", async () => {
const builder = await makeTxBuilder(baseConfig)
.readFrom({ referenceInputs: [scriptCarrier("c".repeat(64))] })
.build(buildOptions)

const fakeTx = await builder.toTransactionWithFakeWitnesses()
expect(fakeTx.witnessSet.vkeyWitnesses?.length ?? 0).toBe(1)
})

it("counts signers for a native-script stake registration credential", async () => {
const requiredNativeScript = NativeScripts.makeScriptPubKey(new Uint8Array(28).fill(0xdd))
const scriptHash = ScriptHash.fromScript(requiredNativeScript)
const state: TxBuilderState = {
...BuilderState.makeInitialTxBuilderState(),
certificates: [new Certificate.StakeRegistration({ stakeCredential: scriptHash })],
referenceInputs: [scriptCarrier("e".repeat(64), requiredNativeScript)]
}
const stateRef = await Effect.runPromise(Ref.make(state))
const input = buildOptions.availableUtxos[0]!

const fakeWitnessSet = await Effect.runPromise(
buildFakeWitnessSet([input]).pipe(Effect.provideService(TxContext, stateRef))
)

// One payment-key witness for the input and one signer for the stake credential.
expect(fakeWitnessSet.vkeyWitnesses?.length ?? 0).toBe(2)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,7 @@ describe("TxBuilder propose with guardrail script: combinations", () => {

// The wallet key and the guardrail's own signer both need a vkey witness
const fakeTx = await signBuilder.toTransactionWithFakeWitnesses()
expect(fakeTx.witnessSet.vkeyWitnesses?.length ?? 0).toBeGreaterThanOrEqual(2)
expect(fakeTx.witnessSet.vkeyWitnesses?.length ?? 0).toBe(2)
})

it("drops a supplied redeemer", async () => {
Expand Down