From a931383f51fcac74304a6e491c2af79c921bc5fd Mon Sep 17 00:00:00 2001 From: mainnet-pat Date: Thu, 26 Jun 2025 07:55:36 +0000 Subject: [PATCH 1/2] Fix TransactionBuilder failing to debug P2PKH-only transactions --- packages/cashscript/src/TransactionBuilder.ts | 6 ++++++ .../cashscript/test/TransactionBuilder.test.ts | 17 +++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/packages/cashscript/src/TransactionBuilder.ts b/packages/cashscript/src/TransactionBuilder.ts index 607a7b93..b30457dd 100644 --- a/packages/cashscript/src/TransactionBuilder.ts +++ b/packages/cashscript/src/TransactionBuilder.ts @@ -18,6 +18,7 @@ import { isUnlockableUtxo, isStandardUnlockableUtxo, StandardUnlockableUtxo, + isP2PKHUnlocker, } from './interfaces.js'; import { NetworkProvider } from './network/index.js'; import { @@ -157,6 +158,11 @@ export class TransactionBuilder { } debug(): DebugResults { + // do not debug a pure P2PKH-spend transaction + if (this.inputs.every((input) => isP2PKHUnlocker(input.unlocker))) { + return {}; + } + if (this.inputs.some((input) => !isStandardUnlockableUtxo(input))) { throw new Error('Cannot debug a transaction with custom unlocker'); } diff --git a/packages/cashscript/test/TransactionBuilder.test.ts b/packages/cashscript/test/TransactionBuilder.test.ts index 4bb73e4e..63e0ee1c 100644 --- a/packages/cashscript/test/TransactionBuilder.test.ts +++ b/packages/cashscript/test/TransactionBuilder.test.ts @@ -9,6 +9,8 @@ import { carolAddress, carolPriv, bobTokenAddress, + aliceAddress, + alicePriv, } from './fixture/vars.js'; import { Network } from '../src/interfaces.js'; import { utxoComparator, calculateDust, randomUtxo, randomToken, isNonTokenUtxo, isFungibleTokenUtxo } from '../src/utils.js'; @@ -293,4 +295,19 @@ describe('Transaction Builder', () => { expect(JSON.parse(stringify(wcTransactionObj))).toEqual(expectedResult); }); }); + + it('should not fail when spending from only P2PKH inputs', async () => { + const aliceUtxos = (await provider.getUtxos(aliceAddress)).filter(isNonTokenUtxo); + const sigTemplate = new SignatureTemplate(alicePriv); + + expect(aliceUtxos.length).toBeGreaterThan(2); + + const change = aliceUtxos[0].satoshis + aliceUtxos[1].satoshis - 1000n; + + await expect(new TransactionBuilder({ provider }) + .addInput(aliceUtxos[0], sigTemplate.unlockP2PKH()) + .addInput(aliceUtxos[1], sigTemplate.unlockP2PKH()) + .addOutput({ to: aliceAddress, amount: change }) + .send()).resolves.not.toThrow(); + }); }); From 621f6dffbefd8e19edbd585b375a2a5f98822b3b Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: Tue, 5 Aug 2025 11:19:41 +0200 Subject: [PATCH 2/2] Add extra tests for intended behaviour with P2PKH-only transactions & with old artifacts --- .../test/TransactionBuilder.test.ts | 44 ++++++++++++-- .../test/debugging-old-artifacts.test.ts | 57 +++++++++++++++++++ 2 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 packages/cashscript/test/debugging-old-artifacts.test.ts diff --git a/packages/cashscript/test/TransactionBuilder.test.ts b/packages/cashscript/test/TransactionBuilder.test.ts index 63e0ee1c..e2321a5c 100644 --- a/packages/cashscript/test/TransactionBuilder.test.ts +++ b/packages/cashscript/test/TransactionBuilder.test.ts @@ -296,7 +296,7 @@ describe('Transaction Builder', () => { }); }); - it('should not fail when spending from only P2PKH inputs', async () => { + it('should not fail when validly spending from only P2PKH inputs', async () => { const aliceUtxos = (await provider.getUtxos(aliceAddress)).filter(isNonTokenUtxo); const sigTemplate = new SignatureTemplate(alicePriv); @@ -304,10 +304,46 @@ describe('Transaction Builder', () => { const change = aliceUtxos[0].satoshis + aliceUtxos[1].satoshis - 1000n; - await expect(new TransactionBuilder({ provider }) + const transaction = new TransactionBuilder({ provider }) .addInput(aliceUtxos[0], sigTemplate.unlockP2PKH()) .addInput(aliceUtxos[1], sigTemplate.unlockP2PKH()) - .addOutput({ to: aliceAddress, amount: change }) - .send()).resolves.not.toThrow(); + .addOutput({ to: aliceAddress, amount: change }); + + await expect(transaction.send()).resolves.not.toThrow(); + }); + + // TODO: Currently, P2PKH inputs are not evaluated at all + it.skip('should fail when invalidly spending from only P2PKH inputs', async () => { + const aliceUtxos = (await provider.getUtxos(aliceAddress)).filter(isNonTokenUtxo); + const incorrectSigTemplate = new SignatureTemplate(bobPriv); + + expect(aliceUtxos.length).toBeGreaterThan(2); + + const change = aliceUtxos[0].satoshis + aliceUtxos[1].satoshis - 1000n; + + const transaction = new TransactionBuilder({ provider }) + .addInput(aliceUtxos[0], incorrectSigTemplate.unlockP2PKH()) + .addInput(aliceUtxos[1], incorrectSigTemplate.unlockP2PKH()) + .addOutput({ to: aliceAddress, amount: change }); + + await expect(transaction.send()).rejects.toThrow(); + }); + + // TODO: Currently, P2PKH inputs are not evaluated at all + it.skip('should fail when invalidly spending from P2PKH and correctly from contract inputs', async () => { + const aliceUtxos = (await provider.getUtxos(aliceAddress)).filter(isNonTokenUtxo); + const p2pkhUtxos = (await p2pkhInstance.getUtxos()).filter(isNonTokenUtxo).sort(utxoComparator).reverse(); + const incorrectSigTemplate = new SignatureTemplate(bobPriv); + + expect(aliceUtxos.length).toBeGreaterThan(2); + + const change = aliceUtxos[0].satoshis + aliceUtxos[1].satoshis - 1000n; + + const transaction = new TransactionBuilder({ provider }) + .addInput(aliceUtxos[0], incorrectSigTemplate.unlockP2PKH()) + .addInput(p2pkhUtxos[0], p2pkhInstance.unlock.spend(carolPub, new SignatureTemplate(carolPriv))) + .addOutput({ to: aliceAddress, amount: change }); + + await expect(transaction.send()).rejects.toThrow(); }); }); diff --git a/packages/cashscript/test/debugging-old-artifacts.test.ts b/packages/cashscript/test/debugging-old-artifacts.test.ts new file mode 100644 index 00000000..35ced7a8 --- /dev/null +++ b/packages/cashscript/test/debugging-old-artifacts.test.ts @@ -0,0 +1,57 @@ +import { Contract, MockNetworkProvider, randomUtxo, SignatureTemplate, TransactionBuilder } from '../src/index.js'; +import { alicePkh, alicePriv, alicePub, bobPriv } from './fixture/vars.js'; + +const artifact = { + contractName: 'P2PKH', + constructorInputs: [ + { name: 'pkh', type: 'bytes20' }, + ], + abi: [ + { + name: 'spend', + inputs: [ + { name: 'pk', type: 'pubkey' }, + { name: 's', type: 'sig' }, + ], + }, + ], + bytecode: 'OP_OVER OP_HASH160 OP_EQUALVERIFY OP_CHECKSIG', + source: 'pragma cashscript ^0.7.0;\n\ncontract P2PKH(bytes20 pkh) {\n // Require pk to match stored pkh and signature to match\n function spend(pubkey pk, sig s) {\n require(hash160(pk) == pkh);\n require(checkSig(s, pk));\n }\n}\n', + compiler: { + name: 'cashc', + version: '0.7.0', + }, + updatedAt: '2025-08-05T09:04:50.388Z', +}; + +describe('Debugging tests - old artifacts', () => { + it('should succeed when passing the correct parameters', () => { + const provider = new MockNetworkProvider(); + const contractTestLogs = new Contract(artifact, [alicePkh], { provider }); + const contractUtxo = randomUtxo(); + provider.addUtxo(contractTestLogs.address, contractUtxo); + + const transaction = new TransactionBuilder({ provider }) + .addInput(contractUtxo, contractTestLogs.unlock.spend(alicePub, new SignatureTemplate(alicePriv))) + .addOutput({ to: contractTestLogs.address, amount: 10000n }); + + console.warn(transaction.bitauthUri()); + + expect(() => transaction.debug()).not.toThrow(); + }); + + it('should fail when passing the wrong parameters', () => { + const provider = new MockNetworkProvider(); + const contractTestLogs = new Contract(artifact, [alicePkh], { provider }); + const contractUtxo = randomUtxo(); + provider.addUtxo(contractTestLogs.address, contractUtxo); + + const transaction = new TransactionBuilder({ provider }) + .addInput(contractUtxo, contractTestLogs.unlock.spend(alicePub, new SignatureTemplate(bobPriv))) + .addOutput({ to: contractTestLogs.address, amount: 10000n }); + + console.warn(transaction.bitauthUri()); + + expect(() => transaction.debug()).toThrow(); + }); +});