Skip to content

Commit 280c566

Browse files
mainnet-patrkalis
andauthored
Fix TransactionBuilder failing to debug P2PKH-only transactions (#318)
Co-authored-by: Rosco Kalis <roscokalis@gmail.com>
1 parent 248870b commit 280c566

File tree

3 files changed

+116
-0
lines changed

3 files changed

+116
-0
lines changed

packages/cashscript/src/TransactionBuilder.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
isUnlockableUtxo,
1919
isStandardUnlockableUtxo,
2020
StandardUnlockableUtxo,
21+
isP2PKHUnlocker,
2122
} from './interfaces.js';
2223
import { NetworkProvider } from './network/index.js';
2324
import {
@@ -157,6 +158,11 @@ export class TransactionBuilder {
157158
}
158159

159160
debug(): DebugResults {
161+
// do not debug a pure P2PKH-spend transaction
162+
if (this.inputs.every((input) => isP2PKHUnlocker(input.unlocker))) {
163+
return {};
164+
}
165+
160166
if (this.inputs.some((input) => !isStandardUnlockableUtxo(input))) {
161167
throw new Error('Cannot debug a transaction with custom unlocker');
162168
}

packages/cashscript/test/TransactionBuilder.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import {
99
carolAddress,
1010
carolPriv,
1111
bobTokenAddress,
12+
aliceAddress,
13+
alicePriv,
1214
} from './fixture/vars.js';
1315
import { Network } from '../src/interfaces.js';
1416
import { utxoComparator, calculateDust, randomUtxo, randomToken, isNonTokenUtxo, isFungibleTokenUtxo } from '../src/utils.js';
@@ -293,4 +295,55 @@ describe('Transaction Builder', () => {
293295
expect(JSON.parse(stringify(wcTransactionObj))).toEqual(expectedResult);
294296
});
295297
});
298+
299+
it('should not fail when validly spending from only P2PKH inputs', async () => {
300+
const aliceUtxos = (await provider.getUtxos(aliceAddress)).filter(isNonTokenUtxo);
301+
const sigTemplate = new SignatureTemplate(alicePriv);
302+
303+
expect(aliceUtxos.length).toBeGreaterThan(2);
304+
305+
const change = aliceUtxos[0].satoshis + aliceUtxos[1].satoshis - 1000n;
306+
307+
const transaction = new TransactionBuilder({ provider })
308+
.addInput(aliceUtxos[0], sigTemplate.unlockP2PKH())
309+
.addInput(aliceUtxos[1], sigTemplate.unlockP2PKH())
310+
.addOutput({ to: aliceAddress, amount: change });
311+
312+
await expect(transaction.send()).resolves.not.toThrow();
313+
});
314+
315+
// TODO: Currently, P2PKH inputs are not evaluated at all
316+
it.skip('should fail when invalidly spending from only P2PKH inputs', async () => {
317+
const aliceUtxos = (await provider.getUtxos(aliceAddress)).filter(isNonTokenUtxo);
318+
const incorrectSigTemplate = new SignatureTemplate(bobPriv);
319+
320+
expect(aliceUtxos.length).toBeGreaterThan(2);
321+
322+
const change = aliceUtxos[0].satoshis + aliceUtxos[1].satoshis - 1000n;
323+
324+
const transaction = new TransactionBuilder({ provider })
325+
.addInput(aliceUtxos[0], incorrectSigTemplate.unlockP2PKH())
326+
.addInput(aliceUtxos[1], incorrectSigTemplate.unlockP2PKH())
327+
.addOutput({ to: aliceAddress, amount: change });
328+
329+
await expect(transaction.send()).rejects.toThrow();
330+
});
331+
332+
// TODO: Currently, P2PKH inputs are not evaluated at all
333+
it.skip('should fail when invalidly spending from P2PKH and correctly from contract inputs', async () => {
334+
const aliceUtxos = (await provider.getUtxos(aliceAddress)).filter(isNonTokenUtxo);
335+
const p2pkhUtxos = (await p2pkhInstance.getUtxos()).filter(isNonTokenUtxo).sort(utxoComparator).reverse();
336+
const incorrectSigTemplate = new SignatureTemplate(bobPriv);
337+
338+
expect(aliceUtxos.length).toBeGreaterThan(2);
339+
340+
const change = aliceUtxos[0].satoshis + aliceUtxos[1].satoshis - 1000n;
341+
342+
const transaction = new TransactionBuilder({ provider })
343+
.addInput(aliceUtxos[0], incorrectSigTemplate.unlockP2PKH())
344+
.addInput(p2pkhUtxos[0], p2pkhInstance.unlock.spend(carolPub, new SignatureTemplate(carolPriv)))
345+
.addOutput({ to: aliceAddress, amount: change });
346+
347+
await expect(transaction.send()).rejects.toThrow();
348+
});
296349
});
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { Contract, MockNetworkProvider, randomUtxo, SignatureTemplate, TransactionBuilder } from '../src/index.js';
2+
import { alicePkh, alicePriv, alicePub, bobPriv } from './fixture/vars.js';
3+
4+
const artifact = {
5+
contractName: 'P2PKH',
6+
constructorInputs: [
7+
{ name: 'pkh', type: 'bytes20' },
8+
],
9+
abi: [
10+
{
11+
name: 'spend',
12+
inputs: [
13+
{ name: 'pk', type: 'pubkey' },
14+
{ name: 's', type: 'sig' },
15+
],
16+
},
17+
],
18+
bytecode: 'OP_OVER OP_HASH160 OP_EQUALVERIFY OP_CHECKSIG',
19+
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',
20+
compiler: {
21+
name: 'cashc',
22+
version: '0.7.0',
23+
},
24+
updatedAt: '2025-08-05T09:04:50.388Z',
25+
};
26+
27+
describe('Debugging tests - old artifacts', () => {
28+
it('should succeed when passing the correct parameters', () => {
29+
const provider = new MockNetworkProvider();
30+
const contractTestLogs = new Contract(artifact, [alicePkh], { provider });
31+
const contractUtxo = randomUtxo();
32+
provider.addUtxo(contractTestLogs.address, contractUtxo);
33+
34+
const transaction = new TransactionBuilder({ provider })
35+
.addInput(contractUtxo, contractTestLogs.unlock.spend(alicePub, new SignatureTemplate(alicePriv)))
36+
.addOutput({ to: contractTestLogs.address, amount: 10000n });
37+
38+
console.warn(transaction.bitauthUri());
39+
40+
expect(() => transaction.debug()).not.toThrow();
41+
});
42+
43+
it('should fail when passing the wrong parameters', () => {
44+
const provider = new MockNetworkProvider();
45+
const contractTestLogs = new Contract(artifact, [alicePkh], { provider });
46+
const contractUtxo = randomUtxo();
47+
provider.addUtxo(contractTestLogs.address, contractUtxo);
48+
49+
const transaction = new TransactionBuilder({ provider })
50+
.addInput(contractUtxo, contractTestLogs.unlock.spend(alicePub, new SignatureTemplate(bobPriv)))
51+
.addOutput({ to: contractTestLogs.address, amount: 10000n });
52+
53+
console.warn(transaction.bitauthUri());
54+
55+
expect(() => transaction.debug()).toThrow();
56+
});
57+
});

0 commit comments

Comments
 (0)