Skip to content

Commit a6726f4

Browse files
mainnet-patrkalis
andauthored
Rework P2PKH template building (#357)
Co-authored-by: Rosco Kalis <[email protected]>
1 parent 37c91a9 commit a6726f4

File tree

10 files changed

+1344
-331
lines changed

10 files changed

+1344
-331
lines changed

packages/cashscript/src/LibauthTemplate.ts

Lines changed: 88 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
binToHex,
44
decodeCashAddress,
55
hexToBin,
6+
Input,
67
isHex,
78
TransactionBch,
89
utf8ToBin,
@@ -30,10 +31,10 @@ import { Contract } from './Contract.js';
3031
import { DebugResults, debugTemplate } from './debugging.js';
3132
import {
3233
HashType,
34+
isContractUnlocker,
3335
isP2PKHUnlocker,
3436
isStandardUnlockableUtxo,
3537
isUnlockableUtxo,
36-
isUtxoP2PKH,
3738
LibauthTokenDetails,
3839
Output,
3940
SignatureAlgorithm,
@@ -43,11 +44,10 @@ import {
4344
Utxo,
4445
} from './interfaces.js';
4546
import SignatureTemplate from './SignatureTemplate.js';
46-
import { addressToLockScript, extendedStringify, zip } from './utils.js';
47+
import { addressToLockScript, extendedStringify, getSignatureAndPubkeyFromP2PKHInput, zip } from './utils.js';
4748
import { TransactionBuilder } from './TransactionBuilder.js';
4849
import { deflate } from 'pako';
4950

50-
5151
/**
5252
* Generates template entities for P2PKH (Pay to Public Key Hash) placeholder scripts.
5353
*
@@ -61,16 +61,22 @@ export const generateTemplateEntitiesP2PKH = (
6161
const lockScriptName = `p2pkh_placeholder_lock_${inputIndex}`;
6262
const unlockScriptName = `p2pkh_placeholder_unlock_${inputIndex}`;
6363

64+
// TODO: Add descriptions
6465
return {
6566
[`signer_${inputIndex}`]: {
6667
scripts: [lockScriptName, unlockScriptName],
67-
description: `placeholder_key_${inputIndex}`,
68+
description: `P2PKH data for input ${inputIndex}`,
6869
name: `P2PKH Signer (input #${inputIndex})`,
6970
variables: {
70-
[`placeholder_key_${inputIndex}`]: {
71+
[`signature_${inputIndex}`]: {
72+
description: '',
73+
name: `P2PKH Signature (input #${inputIndex})`,
74+
type: 'WalletData',
75+
},
76+
[`public_key_${inputIndex}`]: {
7177
description: '',
72-
name: `P2PKH Placeholder Key (input #${inputIndex})`,
73-
type: 'Key',
78+
name: `P2PKH public key (input #${inputIndex})`,
79+
type: 'WalletData',
7480
},
7581
},
7682
},
@@ -152,30 +158,29 @@ const createWalletTemplateVariables = (
152158
*
153159
*/
154160
export const generateTemplateScriptsP2PKH = (
155-
template: SignatureTemplate,
156161
inputIndex: number,
157162
): WalletTemplate['scripts'] => {
158163
const scripts: WalletTemplate['scripts'] = {};
159164
const lockScriptName = `p2pkh_placeholder_lock_${inputIndex}`;
160165
const unlockScriptName = `p2pkh_placeholder_unlock_${inputIndex}`;
161-
const placeholderKeyName = `placeholder_key_${inputIndex}`;
162166

163-
const signatureAlgorithmName = getSignatureAlgorithmName(template.getSignatureAlgorithm());
164-
const hashtypeName = getHashTypeName(template.getHashType(false));
165-
const signatureString = `${placeholderKeyName}.${signatureAlgorithmName}.${hashtypeName}`;
167+
const signatureString = `signature_${inputIndex}`;
168+
const publicKeyString = `public_key_${inputIndex}`;
169+
166170
// add extra unlocking and locking script for P2PKH inputs spent alongside our contract
167171
// this is needed for correct cross-references in the template
168172
scripts[unlockScriptName] = {
173+
passes: [`P2PKH_spend_input${inputIndex}_evaluate`],
169174
name: `P2PKH Unlock (input #${inputIndex})`,
170175
script:
171-
`<${signatureString}>\n<${placeholderKeyName}.public_key>`,
176+
`<${signatureString}>\n<${publicKeyString}>`,
172177
unlocks: lockScriptName,
173178
};
174179
scripts[lockScriptName] = {
175180
lockingType: 'standard',
176181
name: `P2PKH Lock (input #${inputIndex})`,
177182
script:
178-
`OP_DUP\nOP_HASH160 <$(<${placeholderKeyName}.public_key> OP_HASH160\n)> OP_EQUALVERIFY\nOP_CHECKSIG`,
183+
`OP_DUP\nOP_HASH160 <$(<${publicKeyString}> OP_HASH160\n)> OP_EQUALVERIFY\nOP_CHECKSIG`,
179184
};
180185

181186
return scripts;
@@ -275,6 +280,7 @@ export const generateTemplateScenarios = (
275280
const encodedConstructorArgs = contract.encodedConstructorArgs;
276281
const scenarioIdentifier = `${artifact.contractName}_${abiFunction.name}_input${inputIndex}_evaluate`;
277282

283+
// TODO: Update scenario descriptions
278284
const scenarios = {
279285
// single scenario to spend out transaction under test given the CashScript parameters provided
280286
[scenarioIdentifier]: {
@@ -288,12 +294,13 @@ export const generateTemplateScenarios = (
288294
},
289295
currentBlockHeight: 2,
290296
currentBlockTime: Math.round(+new Date() / 1000),
297+
// TODO: remove usage of private keys in P2SH scenarios as well
291298
keys: {
292299
privateKeys: generateTemplateScenarioKeys(abiFunction.inputs, encodedFunctionArgs),
293300
},
294301
},
295302
transaction: generateTemplateScenarioTransaction(contract, libauthTransaction, csTransaction, inputIndex),
296-
sourceOutputs: generateTemplateScenarioSourceOutputs(csTransaction, inputIndex),
303+
sourceOutputs: generateTemplateScenarioSourceOutputs(csTransaction, libauthTransaction, inputIndex),
297304
},
298305
};
299306

@@ -308,20 +315,53 @@ export const generateTemplateScenarios = (
308315
return scenarios;
309316
};
310317

318+
export const generateTemplateScenariosP2PKH = (
319+
libauthTransaction: TransactionBch,
320+
csTransaction: TransactionType,
321+
inputIndex: number,
322+
): WalletTemplate['scenarios'] => {
323+
const scenarioIdentifier = `P2PKH_spend_input${inputIndex}_evaluate`;
324+
325+
const { signature, publicKey } = getSignatureAndPubkeyFromP2PKHInput(libauthTransaction.inputs[inputIndex]);
326+
327+
// TODO: Update scenario descriptions
328+
const scenarios = {
329+
// single scenario to spend out transaction under test given the CashScript parameters provided
330+
[scenarioIdentifier]: {
331+
name: `Evaluate P2PKH spend (input #${inputIndex})`,
332+
description: 'An example evaluation where this script execution passes.',
333+
data: {
334+
// encode values for the variables defined above in `entities` property
335+
bytecode: {
336+
[`signature_${inputIndex}`]: `0x${binToHex(signature)}`,
337+
[`public_key_${inputIndex}`]: `0x${binToHex(publicKey)}`,
338+
},
339+
currentBlockHeight: 2,
340+
currentBlockTime: Math.round(+new Date() / 1000),
341+
},
342+
transaction: generateTemplateScenarioTransaction(undefined, libauthTransaction, csTransaction, inputIndex),
343+
sourceOutputs: generateTemplateScenarioSourceOutputs(csTransaction, libauthTransaction, inputIndex),
344+
},
345+
};
346+
347+
return scenarios;
348+
};
349+
311350
const generateTemplateScenarioTransaction = (
312-
contract: Contract,
351+
contract: Contract | undefined,
313352
libauthTransaction: TransactionBch,
314353
csTransaction: TransactionType,
315354
slotIndex: number,
316355
): WalletTemplateScenario['transaction'] => {
317356
const inputs = libauthTransaction.inputs.map((input, inputIndex) => {
318357
const csInput = csTransaction.inputs[inputIndex] as Utxo;
358+
const libauthInput = libauthTransaction.inputs[inputIndex];
319359

320360
return {
321361
outpointIndex: input.outpointIndex,
322362
outpointTransactionHash: binToHex(input.outpointTransactionHash),
323363
sequenceNumber: input.sequenceNumber,
324-
unlockingBytecode: generateTemplateScenarioBytecode(csInput, inputIndex, 'p2pkh_placeholder_unlock', slotIndex === inputIndex),
364+
unlockingBytecode: generateTemplateScenarioBytecode(csInput, libauthInput, inputIndex, 'p2pkh_placeholder_unlock', slotIndex === inputIndex),
325365
} as WalletTemplateScenarioInput;
326366
});
327367

@@ -330,8 +370,16 @@ const generateTemplateScenarioTransaction = (
330370
const outputs = libauthTransaction.outputs.map((output, index) => {
331371
const csOutput = csTransaction.outputs[index];
332372

373+
if (csOutput && contract) {
374+
return {
375+
lockingBytecode: generateTemplateScenarioTransactionOutputLockingBytecode(csOutput, contract),
376+
token: serialiseTokenDetails(output.token),
377+
valueSatoshis: Number(output.valueSatoshis),
378+
} as WalletTemplateScenarioTransactionOutput;
379+
}
380+
333381
return {
334-
lockingBytecode: generateTemplateScenarioTransactionOutputLockingBytecode(csOutput, contract),
382+
lockingBytecode: `${binToHex(output.lockingBytecode)}`,
335383
token: serialiseTokenDetails(output.token),
336384
valueSatoshis: Number(output.valueSatoshis),
337385
} as WalletTemplateScenarioTransactionOutput;
@@ -344,11 +392,14 @@ const generateTemplateScenarioTransaction = (
344392

345393
const generateTemplateScenarioSourceOutputs = (
346394
csTransaction: TransactionType,
395+
libauthTransaction: TransactionBch,
347396
slotIndex: number,
348397
): Array<WalletTemplateScenarioOutput<true>> => {
349398
return csTransaction.inputs.map((input, inputIndex) => {
399+
const libauthInput = libauthTransaction.inputs[inputIndex];
400+
350401
return {
351-
lockingBytecode: generateTemplateScenarioBytecode(input, inputIndex, 'p2pkh_placeholder_lock', inputIndex === slotIndex),
402+
lockingBytecode: generateTemplateScenarioBytecode(input, libauthInput, inputIndex, 'p2pkh_placeholder_lock', inputIndex === slotIndex),
352403
valueSatoshis: Number(input.satoshis),
353404
token: serialiseTokenDetails(input.token),
354405
};
@@ -409,18 +460,14 @@ export const getLibauthTemplates = (
409460

410461
// We can typecast this because we check that all inputs are standard unlockable at the top of this function
411462
for (const [inputIndex, input] of (txn.inputs as StandardUnlockableUtxo[]).entries()) {
412-
// If template exists on the input, it indicates this is a P2PKH (Pay to Public Key Hash) input
413-
if ('template' in input.unlocker) {
414-
// @ts-ignore TODO: Remove UtxoP2PKH type and only use UnlockableUtxo in Libauth Template generation
415-
input.template = input.unlocker?.template; // Added to support P2PKH inputs in buildTemplate
463+
if (isP2PKHUnlocker(input.unlocker)) {
416464
Object.assign(p2pkhEntities, generateTemplateEntitiesP2PKH(inputIndex));
417-
Object.assign(p2pkhScripts, generateTemplateScriptsP2PKH(input.unlocker.template, inputIndex));
418-
465+
Object.assign(p2pkhScripts, generateTemplateScriptsP2PKH(inputIndex));
466+
Object.assign(scenarios, generateTemplateScenariosP2PKH(libauthTransaction, csTransaction, inputIndex));
419467
continue;
420468
}
421469

422-
// If contract exists on the input, it indicates this is a contract input
423-
if ('contract' in input.unlocker) {
470+
if (isContractUnlocker(input.unlocker)) {
424471
const contract = input.unlocker?.contract;
425472
const abiFunction = input.unlocker?.abiFunction;
426473

@@ -537,7 +584,7 @@ export const getLibauthTemplates = (
537584

538585
export const debugLibauthTemplate = (template: WalletTemplate, transaction: TransactionBuilder): DebugResults => {
539586
const allArtifacts = transaction.inputs
540-
.map(input => 'contract' in input.unlocker ? input.unlocker.contract : undefined)
587+
.map(input => isContractUnlocker(input.unlocker) ? input.unlocker.contract : undefined)
541588
.filter((contract): contract is Contract => Boolean(contract))
542589
.map(contract => contract.artifact);
543590

@@ -575,17 +622,19 @@ const generateLockingScriptParams = (
575622

576623
export const generateUnlockingScriptParams = (
577624
csInput: StandardUnlockableUtxo,
625+
libauthInput: Input,
578626
p2pkhScriptNameTemplate: string,
579627
inputIndex: number,
580628
): WalletTemplateScenarioBytecode => {
581629
if (isP2PKHUnlocker(csInput.unlocker)) {
630+
const { signature, publicKey } = getSignatureAndPubkeyFromP2PKHInput(libauthInput);
631+
582632
return {
583633
script: `${p2pkhScriptNameTemplate}_${inputIndex}`,
584634
overrides: {
585-
keys: {
586-
privateKeys: {
587-
[`placeholder_key_${inputIndex}`]: binToHex(csInput.unlocker.template.privateKey),
588-
},
635+
bytecode: {
636+
[`signature_${inputIndex}`]: `0x${binToHex(signature)}`,
637+
[`public_key_${inputIndex}`]: `0x${binToHex(publicKey)}`,
589638
},
590639
},
591640
};
@@ -604,6 +653,7 @@ export const generateUnlockingScriptParams = (
604653
...generateTemplateScenarioParametersValues(abiFunction.inputs, encodedFunctionArgs),
605654
...generateTemplateScenarioParametersValues(contract.artifact.constructorInputs, contract.encodedConstructorArgs),
606655
},
656+
// TODO: remove usage of private keys in P2SH scenarios as well
607657
keys: {
608658
privateKeys: generateTemplateScenarioKeys(abiFunction.inputs, encodedFunctionArgs),
609659
},
@@ -745,29 +795,16 @@ export const generateTemplateScenarioKeys = (
745795

746796
// Used for generating the locking / unlocking bytecode for source outputs and inputs
747797
export const generateTemplateScenarioBytecode = (
748-
input: Utxo, inputIndex: number, p2pkhScriptNameTemplate: string, insertSlot?: boolean,
798+
input: Utxo,
799+
libauthInput: Input,
800+
inputIndex: number,
801+
p2pkhScriptNameTemplate: string,
802+
insertSlot?: boolean,
749803
): WalletTemplateScenarioBytecode | ['slot'] => {
750804
if (insertSlot) return ['slot'];
751805

752-
const p2pkhScriptName = `${p2pkhScriptNameTemplate}_${inputIndex}`;
753-
const placeholderKeyName = `placeholder_key_${inputIndex}`;
754-
755-
// This is for P2PKH inputs in the old transaction builder (TODO: remove when we remove old transaction builder)
756-
if (isUtxoP2PKH(input)) {
757-
return {
758-
script: p2pkhScriptName,
759-
overrides: {
760-
keys: {
761-
privateKeys: {
762-
[placeholderKeyName]: binToHex(input.template.privateKey),
763-
},
764-
},
765-
},
766-
};
767-
}
768-
769806
if (isUnlockableUtxo(input) && isStandardUnlockableUtxo(input)) {
770-
return generateUnlockingScriptParams(input, p2pkhScriptNameTemplate, inputIndex);
807+
return generateUnlockingScriptParams(input, libauthInput, p2pkhScriptNameTemplate, inputIndex);
771808
}
772809

773810
// 'slot' means that we are currently evaluating this specific input,

packages/cashscript/src/TransactionBuilder.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import {
1717
isUnlockableUtxo,
1818
isStandardUnlockableUtxo,
1919
StandardUnlockableUtxo,
20-
isP2PKHUnlocker,
2120
} from './interfaces.js';
2221
import { NetworkProvider } from './network/index.js';
2322
import {
@@ -157,11 +156,6 @@ export class TransactionBuilder {
157156
}
158157

159158
debug(): DebugResults {
160-
// do not debug a pure P2PKH-spend transaction
161-
if (this.inputs.every((input) => isP2PKHUnlocker(input.unlocker))) {
162-
return {};
163-
}
164-
165159
if (this.inputs.some((input) => !isStandardUnlockableUtxo(input))) {
166160
throw new Error('Cannot debug a transaction with custom unlocker');
167161
}

0 commit comments

Comments
 (0)