Skip to content

Commit 981eb07

Browse files
authored
Breaking changes to SignatureTemplate (#440)
- Rename HashType to SighashType - Change SignatureTemplate getters to simple properties - Update docs, release notes & migration notes
1 parent 45b3fbc commit 981eb07

12 files changed

Lines changed: 148 additions & 113 deletions

File tree

‎.cspell.json‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,6 @@
8686
"hashprevouts",
8787
"hashsequence",
8888
"hashtype",
89-
"hashtypes",
9089
"hodl",
9190
"hodling",
9291
"htlc",

‎packages/cashscript/src/Contract.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ class ContractInternal<
195195
if (!(arg instanceof SignatureTemplate)) return arg;
196196

197197
// Generate transaction signature from SignatureTemplate
198-
const preimage = createSighashPreimage(transaction, sourceOutputs, inputIndex, bytecode, arg.getHashType());
198+
const preimage = createSighashPreimage(transaction, sourceOutputs, inputIndex, bytecode, arg.sighashType);
199199
const sighash = hash256(preimage);
200200
return arg.generateSignature(sighash);
201201
});

‎packages/cashscript/src/SignatureTemplate.ts‎

Lines changed: 20 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,40 @@
1-
import { decodePrivateKeyWif, hexToBin, isHex, secp256k1, SigningSerializationFlag } from '@bitauth/libauth';
1+
import { decodePrivateKeyWif, hexToBin, isHex, secp256k1 } from '@bitauth/libauth';
22
import { hash256, scriptToBytecode } from '@cashscript/utils';
33
import {
44
GenerateUnlockingBytecodeOptions,
5-
HashType,
5+
SighashType,
66
SignatureAlgorithm,
77
P2PKHUnlocker,
88
} from './interfaces.js';
9-
import { createSighashPreimage, publicKeyToP2PKHLockingBytecode } from './utils.js';
9+
import { createSighashPreimage, publicKeyToP2PKHLockingBytecode, toSigningSerializationType } from './utils.js';
1010

1111
/**
1212
* A signature template used to sign CashScript transactions. Wraps a private key together with
13-
* the desired `HashType` and `SignatureAlgorithm`, and is consumed by the `TransactionBuilder`
13+
* the desired `SighashType` and `SignatureAlgorithm`, and is consumed by the `TransactionBuilder`
1414
* whenever a `sig` argument is required or when unlocking a P2PKH input.
1515
*/
1616
export default class SignatureTemplate {
1717
/** The raw private key bytes used for signing. */
1818
public privateKey: Uint8Array;
1919

20+
/** The 33-byte compressed public key that corresponds to the template's private key. */
21+
get publicKey(): Uint8Array {
22+
return secp256k1.derivePublicKeyCompressed(this.privateKey) as Uint8Array;
23+
}
24+
2025
/**
2126
* Create a new SignatureTemplate.
2227
*
2328
* @param signer - A 32-byte private key (Uint8Array), a WIF or hex-encoded private key string,
2429
* or any object exposing a `toWIF()` method (e.g. bitcore-lib or bitcoincashjs `ECPair`).
25-
* @param hashtype - Sighash flags to use when signing. Defaults to `SIGHASH_ALL | SIGHASH_UTXOS`.
30+
* @param sighashType - Sighash flags to use when signing. Defaults to `SIGHASH_ALL | SIGHASH_UTXOS`.
2631
* @param signatureAlgorithm - The signature algorithm to use. Defaults to
2732
* `SignatureAlgorithm.SCHNORR`.
2833
*/
2934
constructor(
3035
signer: Keypair | Uint8Array | string,
31-
private hashtype: HashType = HashType.SIGHASH_ALL | HashType.SIGHASH_UTXOS,
32-
private signatureAlgorithm: SignatureAlgorithm = SignatureAlgorithm.SCHNORR,
36+
public readonly sighashType: SighashType = SighashType.SIGHASH_ALL | SighashType.SIGHASH_UTXOS,
37+
public readonly signatureAlgorithm: SignatureAlgorithm = SignatureAlgorithm.SCHNORR,
3338
) {
3439
if (isKeypair(signer)) {
3540
const wif = signer.toWIF();
@@ -47,24 +52,22 @@ export default class SignatureTemplate {
4752
}
4853

4954
/**
50-
* Sign the provided sighash payload and return the signature concatenated with the hashtype
55+
* Sign the provided sighash payload and return the signature concatenated with the sighash type
5156
* byte, ready to be used as a transaction signature.
5257
*
5358
* @param payload - The 32-byte sighash to sign.
54-
* @param bchForkId - Whether to include the BCH fork id flag in the appended hashtype byte.
55-
* Defaults to `true`.
56-
* @returns The signature bytes followed by the hashtype byte.
59+
* @returns The signature bytes followed by the sighash type byte.
5760
*/
58-
generateSignature(payload: Uint8Array, bchForkId?: boolean): Uint8Array {
61+
generateSignature(payload: Uint8Array): Uint8Array {
5962
const signature = this.signMessageHash(payload);
60-
return Uint8Array.from([...signature, this.getHashType(bchForkId)]);
63+
return Uint8Array.from([...signature, toSigningSerializationType(this.sighashType)]);
6164
}
6265

6366
/**
6467
* Sign a raw 32-byte message hash using the template's private key and signature algorithm.
6568
*
6669
* @param payload - The 32-byte hash to sign.
67-
* @returns The raw signature bytes (without an appended hashtype byte).
70+
* @returns The raw signature bytes (without an appended sighash type byte).
6871
*/
6972
signMessageHash(payload: Uint8Array): Uint8Array {
7073
const signature = this.signatureAlgorithm === SignatureAlgorithm.SCHNORR
@@ -74,50 +77,22 @@ export default class SignatureTemplate {
7477
return signature;
7578
}
7679

77-
/**
78-
* Get the sighash flags used by this template.
79-
*
80-
* @param bchForkId - Whether to OR in the BCH fork id flag. Defaults to `true`.
81-
* @returns The combined hashtype byte.
82-
*/
83-
getHashType(bchForkId: boolean = true): number {
84-
return bchForkId ? (this.hashtype | SigningSerializationFlag.forkId) : this.hashtype;
85-
}
86-
87-
/**
88-
* @returns The signature algorithm (ECDSA or Schnorr) used by this template.
89-
*/
90-
getSignatureAlgorithm(): SignatureAlgorithm {
91-
return this.signatureAlgorithm;
92-
}
93-
94-
/**
95-
* Derive the compressed public key that corresponds to the template's private key.
96-
*
97-
* @returns The 33-byte compressed public key.
98-
*/
99-
getPublicKey(): Uint8Array {
100-
return secp256k1.derivePublicKeyCompressed(this.privateKey) as Uint8Array;
101-
}
102-
10380
/**
10481
* Build a P2PKH `Unlocker` for the address derived from this template's private key. The
10582
* returned unlocker can be passed directly to `TransactionBuilder.addInput`.
10683
*
10784
* @returns An unlocker that signs the corresponding P2PKH UTXO.
10885
*/
10986
unlockP2PKH(): P2PKHUnlocker {
110-
const publicKey = this.getPublicKey();
111-
const prevOutScript = publicKeyToP2PKHLockingBytecode(publicKey);
112-
const hashtype = this.getHashType();
87+
const prevOutScript = publicKeyToP2PKHLockingBytecode(this.publicKey);
11388

11489
return {
11590
generateLockingBytecode: () => prevOutScript,
11691
generateUnlockingBytecode: ({ transaction, sourceOutputs, inputIndex }: GenerateUnlockingBytecodeOptions) => {
117-
const preimage = createSighashPreimage(transaction, sourceOutputs, inputIndex, prevOutScript, hashtype);
92+
const preimage = createSighashPreimage(transaction, sourceOutputs, inputIndex, prevOutScript, this.sighashType);
11893
const sighash = hash256(preimage);
11994
const signature = this.generateSignature(sighash);
120-
const unlockingBytecode = scriptToBytecode([signature, publicKey]);
95+
const unlockingBytecode = scriptToBytecode([signature, this.publicKey]);
12196
return unlockingBytecode;
12297
},
12398
template: this,

‎packages/cashscript/src/interfaces.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ export enum SignatureAlgorithm {
133133
SCHNORR = 0x01,
134134
}
135135

136-
export enum HashType {
136+
export enum SighashType {
137137
SIGHASH_ALL = 0x01,
138138
SIGHASH_NONE = 0x02,
139139
SIGHASH_SINGLE = 0x03,

‎packages/cashscript/src/libauth-template/utils.ts‎

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { AbiFunction, AbiInput, Artifact, formatBitAuthScript, sha256 } from '@cashscript/utils';
2-
import { HashType, LibauthTokenDetails, SignatureAlgorithm, TokenDetails, VmTarget } from '../interfaces.js';
2+
import { LibauthTokenDetails, SighashType, SignatureAlgorithm, TokenDetails, VmTarget } from '../interfaces.js';
33
import { hexToBin, binToHex, isHex, decodeCashAddress, Input, assertSuccess, decodeAuthenticationInstructions, AuthenticationInstructionPush } from '@bitauth/libauth';
44
import { EncodedFunctionArgument } from '../Argument.js';
55
import { zip } from '../utils.js';
@@ -32,23 +32,23 @@ export const getSignatureAlgorithmName = (signatureAlgorithm: SignatureAlgorithm
3232
return signatureAlgorithmNames[signatureAlgorithm];
3333
};
3434

35-
export const getHashTypeName = (hashType: HashType): string => {
36-
const hashtypeNames = {
37-
[HashType.SIGHASH_ALL]: 'all_outputs',
38-
[HashType.SIGHASH_ALL | HashType.SIGHASH_ANYONECANPAY]: 'all_outputs_single_input',
39-
[HashType.SIGHASH_ALL | HashType.SIGHASH_UTXOS]: 'all_outputs_all_utxos',
40-
[HashType.SIGHASH_ALL | HashType.SIGHASH_ANYONECANPAY | HashType.SIGHASH_UTXOS]: 'all_outputs_single_input_INVALID_all_utxos',
41-
[HashType.SIGHASH_SINGLE]: 'corresponding_output',
42-
[HashType.SIGHASH_SINGLE | HashType.SIGHASH_ANYONECANPAY]: 'corresponding_output_single_input',
43-
[HashType.SIGHASH_SINGLE | HashType.SIGHASH_UTXOS]: 'corresponding_output_all_utxos',
44-
[HashType.SIGHASH_SINGLE | HashType.SIGHASH_ANYONECANPAY | HashType.SIGHASH_UTXOS]: 'corresponding_output_single_input_INVALID_all_utxos',
45-
[HashType.SIGHASH_NONE]: 'no_outputs',
46-
[HashType.SIGHASH_NONE | HashType.SIGHASH_ANYONECANPAY]: 'no_outputs_single_input',
47-
[HashType.SIGHASH_NONE | HashType.SIGHASH_UTXOS]: 'no_outputs_all_utxos',
48-
[HashType.SIGHASH_NONE | HashType.SIGHASH_ANYONECANPAY | HashType.SIGHASH_UTXOS]: 'no_outputs_single_input_INVALID_all_utxos',
35+
export const getSighashTypeName = (sighashType: SighashType): string => {
36+
const sighashTypeNames = {
37+
[SighashType.SIGHASH_ALL]: 'all_outputs',
38+
[SighashType.SIGHASH_ALL | SighashType.SIGHASH_ANYONECANPAY]: 'all_outputs_single_input',
39+
[SighashType.SIGHASH_ALL | SighashType.SIGHASH_UTXOS]: 'all_outputs_all_utxos',
40+
[SighashType.SIGHASH_ALL | SighashType.SIGHASH_ANYONECANPAY | SighashType.SIGHASH_UTXOS]: 'all_outputs_single_input_INVALID_all_utxos',
41+
[SighashType.SIGHASH_SINGLE]: 'corresponding_output',
42+
[SighashType.SIGHASH_SINGLE | SighashType.SIGHASH_ANYONECANPAY]: 'corresponding_output_single_input',
43+
[SighashType.SIGHASH_SINGLE | SighashType.SIGHASH_UTXOS]: 'corresponding_output_all_utxos',
44+
[SighashType.SIGHASH_SINGLE | SighashType.SIGHASH_ANYONECANPAY | SighashType.SIGHASH_UTXOS]: 'corresponding_output_single_input_INVALID_all_utxos',
45+
[SighashType.SIGHASH_NONE]: 'no_outputs',
46+
[SighashType.SIGHASH_NONE | SighashType.SIGHASH_ANYONECANPAY]: 'no_outputs_single_input',
47+
[SighashType.SIGHASH_NONE | SighashType.SIGHASH_UTXOS]: 'no_outputs_all_utxos',
48+
[SighashType.SIGHASH_NONE | SighashType.SIGHASH_ANYONECANPAY | SighashType.SIGHASH_UTXOS]: 'no_outputs_single_input_INVALID_all_utxos',
4949
};
5050

51-
return hashtypeNames[hashType];
51+
return sighashTypeNames[sighashType];
5252
};
5353

5454
export const addHexPrefixExceptEmpty = (value: string): string => {
@@ -63,9 +63,9 @@ export const formatParametersForDebugging = (types: readonly AbiInput[], args: E
6363

6464
return typesAndArguments.map(([input, arg]) => {
6565
if (arg instanceof SignatureTemplate) {
66-
const signatureAlgorithmName = getSignatureAlgorithmName(arg.getSignatureAlgorithm());
67-
const hashtypeName = getHashTypeName(arg.getHashType(false));
68-
return `<${input.name}.${signatureAlgorithmName}.${hashtypeName}> // ${input.type}`;
66+
const signatureAlgorithmName = getSignatureAlgorithmName(arg.signatureAlgorithm);
67+
const sighashTypeName = getSighashTypeName(arg.sighashType);
68+
return `<${input.name}.${signatureAlgorithmName}.${sighashTypeName}> // ${input.type}`;
6969
}
7070

7171
const typeStr = input.type === 'bytes' ? `bytes${arg.length}` : input.type;

‎packages/cashscript/src/utils.ts‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
bigIntToCompactUint,
1515
NonFungibleTokenCapability,
1616
bigIntToVmNumber,
17+
SigningSerializationFlag,
1718
} from '@bitauth/libauth';
1819
import {
1920
encodeInt,
@@ -35,6 +36,7 @@ import {
3536
UnlockableUtxo,
3637
LibauthTokenDetails,
3738
ContractType,
39+
SighashType,
3840
} from './interfaces.js';
3941
import { VERSION_SIZE, LOCKTIME_SIZE } from './constants.js';
4042
import {
@@ -265,15 +267,20 @@ function toBin(output: string): Uint8Array {
265267
return encode(data);
266268
}
267269

270+
// BCH consensus requires the fork id flag on every signing serialization
271+
export function toSigningSerializationType(sighashType: SighashType): number {
272+
return sighashType | SigningSerializationFlag.forkId;
273+
}
274+
268275
export function createSighashPreimage(
269276
transaction: Transaction,
270277
sourceOutputs: LibauthOutput[],
271278
inputIndex: number,
272279
coveredBytecode: Uint8Array,
273-
hashtype: number,
280+
sighashType: SighashType,
274281
): Uint8Array {
275282
const context = { inputIndex, sourceOutputs, transaction };
276-
const signingSerializationType = new Uint8Array([hashtype]);
283+
const signingSerializationType = new Uint8Array([toSigningSerializationType(sighashType)]);
277284

278285
const sighashPreimage = generateSigningSerializationBch(context, { coveredBytecode, signingSerializationType });
279286

‎packages/cashscript/test/SignatureTemplate.test.ts‎

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { generateLibauthSourceOutputs } from 'cashscript/dist/utils.js';
2-
import { HashType, MockNetworkProvider, SignatureAlgorithm, SignatureTemplate, TransactionBuilder } from '../src/index.js';
2+
import { MockNetworkProvider, SighashType, SignatureAlgorithm, SignatureTemplate, TransactionBuilder } from '../src/index.js';
33
import { aliceAddress, alicePriv, alicePub, aliceWif } from './fixture/vars.js';
44
import { binToHex, decodeTransactionUnsafe, hexToBin } from '@bitauth/libauth';
55

@@ -33,17 +33,11 @@ describe('SignatureTemplate', () => {
3333
expect(signature).toEqual(hexToBin('3045022100fa1d6a159a124e99479f78152422d55ff3c16f7fac5ae47fa291907f8f47613f02200d6c906f667b3712860b6f5a1f296ecb7dcd44da83c6a1eb45869b61c6b8dadb61'));
3434
});
3535

36-
it('should append the correct hash type when fork ID is true', () => {
37-
const signatureTemplate = new SignatureTemplate(alicePriv, HashType.SIGHASH_SINGLE);
38-
const signature = signatureTemplate.generateSignature(hexToBin('0000000000000000000000'), true);
36+
it('should append the configured sighash type, always including the BCH fork ID', () => {
37+
const signatureTemplate = new SignatureTemplate(alicePriv, SighashType.SIGHASH_SINGLE);
38+
const signature = signatureTemplate.generateSignature(hexToBin('0000000000000000000000'));
3939
expect(signature).toEqual(hexToBin('bcac180e17de108003cce026708bd2af54b860dad2626cee157f4ed5abd993b9085d615015f905978adc51e8878226280ddd27d899f086519c0978e53332d79943'));
4040
});
41-
42-
it('should append the correct hash type when fork ID is false', () => {
43-
const signatureTemplate = new SignatureTemplate(alicePriv, HashType.SIGHASH_SINGLE);
44-
const signature = signatureTemplate.generateSignature(hexToBin('0000000000000000000000'), false);
45-
expect(signature).toEqual(hexToBin('bcac180e17de108003cce026708bd2af54b860dad2626cee157f4ed5abd993b9085d615015f905978adc51e8878226280ddd27d899f086519c0978e53332d79903'));
46-
});
4741
});
4842

4943
describe('signMessageHash', () => {
@@ -62,10 +56,10 @@ describe('SignatureTemplate', () => {
6256
});
6357
});
6458

65-
describe('getPublicKey', () => {
59+
describe('publicKey', () => {
6660
it('should generate a correct public key', () => {
6761
const signatureTemplate = new SignatureTemplate(alicePriv);
68-
expect(signatureTemplate.getPublicKey()).toEqual(alicePub);
62+
expect(signatureTemplate.publicKey).toEqual(alicePub);
6963
});
7064
});
7165

‎packages/cashscript/test/e2e/HodlVault.test.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
Network,
77
TransactionBuilder,
88
SignatureAlgorithm,
9-
HashType,
9+
SighashType,
1010
} from '../../src/index.js';
1111
import {
1212
alicePriv,
@@ -108,7 +108,7 @@ describe('HodlVault', () => {
108108
const amount = 10000n;
109109
const { utxos, changeAmount } = gatherUtxos(await hodlVault.getUtxos(), { amount, fee: 2000n });
110110

111-
const signatureTemplate = new SignatureTemplate(alicePriv, HashType.SIGHASH_ALL, SignatureAlgorithm.ECDSA);
111+
const signatureTemplate = new SignatureTemplate(alicePriv, SighashType.SIGHASH_ALL, SignatureAlgorithm.ECDSA);
112112

113113
// when
114114
const tx = await new TransactionBuilder({ provider })
@@ -185,7 +185,7 @@ describe('HodlVault', () => {
185185
.send()).rejects.toThrow('HodlVault.cash:31 Require statement failed at input 0 in contract HodlVault.cash at line 31');
186186

187187
// datasig: unlocker should throw when given an improper length
188-
const signatureTemplate = new SignatureTemplate(alicePriv, HashType.SIGHASH_ALL, SignatureAlgorithm.ECDSA);
188+
const signatureTemplate = new SignatureTemplate(alicePriv, SighashType.SIGHASH_ALL, SignatureAlgorithm.ECDSA);
189189
expect(() => hodlVault.unlock.spend(signatureTemplate, placeholder(100), message)).toThrow("Found type 'bytes100' where type 'datasig' was expected");
190190

191191
// datasig: unlocker should not throw when given a proper length, but transaction should fail on invalid sig

‎packages/cashscript/test/fixture/libauth-template/fixtures.ts‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Contract, HashType, MockNetworkProvider, SignatureAlgorithm, SignatureTemplate, TransactionBuilder, randomNFT, randomToken, randomUtxo } from '../../../src/index.js';
1+
import { Contract, MockNetworkProvider, SighashType, SignatureAlgorithm, SignatureTemplate, TransactionBuilder, randomNFT, randomToken, randomUtxo } from '../../../src/index.js';
22
import TransferWithTimeout from '../transfer_with_timeout.artifact.js';
33
import Mecenas from '../mecenas.artifact.js';
44
import P2PKH from '../p2pkh.artifact.js';
@@ -1262,7 +1262,7 @@ export const fixtures: Fixture[] = [
12621262
},
12631263
},
12641264
},
1265-
// TODO: Make it work with different hashtypes and signature algorithms
1265+
// TODO: Make it work with different sighash types and signature algorithms
12661266
// {
12671267
// name: 'P2PKH (sending NFTs)',
12681268
// transaction: (() => {
@@ -1272,11 +1272,11 @@ export const fixtures: Fixture[] = [
12721272
// const to = contract.address;
12731273
// const amount = 1000n;
12741274

1275-
// const hashtype = HashType.SIGHASH_SINGLE | HashType.SIGHASH_ANYONECANPAY;
1275+
// const sighashType = SighashType.SIGHASH_SINGLE | SighashType.SIGHASH_ANYONECANPAY;
12761276
// const signatureAlgorithm = SignatureAlgorithm.ECDSA;
12771277

12781278
// const tx = contract.functions
1279-
// .spend(alicePub, new SignatureTemplate(alicePriv, hashtype, signatureAlgorithm))
1279+
// .spend(alicePub, new SignatureTemplate(alicePriv, sighashType, signatureAlgorithm))
12801280
// .to(to, amount);
12811281

12821282
// return tx;
@@ -1295,8 +1295,8 @@ export const fixtures: Fixture[] = [
12951295
const amount = 1000n;
12961296

12971297
const aliceDefaultTemplate = new SignatureTemplate(alicePriv);
1298-
const aliceCustomTemplate = new SignatureTemplate(alicePriv, HashType.SIGHASH_NONE, SignatureAlgorithm.ECDSA);
1299-
const bobCustomTemplate = new SignatureTemplate(bobPriv, HashType.SIGHASH_ALL, SignatureAlgorithm.ECDSA);
1298+
const aliceCustomTemplate = new SignatureTemplate(alicePriv, SighashType.SIGHASH_NONE, SignatureAlgorithm.ECDSA);
1299+
const bobCustomTemplate = new SignatureTemplate(bobPriv, SighashType.SIGHASH_ALL, SignatureAlgorithm.ECDSA);
13001300

13011301
const tx = new TransactionBuilder({ provider })
13021302
.addInput(p2pkhUtxo, aliceDefaultTemplate.unlockP2PKH())

0 commit comments

Comments
 (0)