Skip to content
Merged
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
1 change: 0 additions & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@
"hashprevouts",
"hashsequence",
"hashtype",
"hashtypes",
"hodl",
"hodling",
"htlc",
Expand Down
2 changes: 1 addition & 1 deletion packages/cashscript/src/Contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ class ContractInternal<
if (!(arg instanceof SignatureTemplate)) return arg;

// Generate transaction signature from SignatureTemplate
const preimage = createSighashPreimage(transaction, sourceOutputs, inputIndex, bytecode, arg.getHashType());
const preimage = createSighashPreimage(transaction, sourceOutputs, inputIndex, bytecode, arg.sighashType);
const sighash = hash256(preimage);
return arg.generateSignature(sighash);
});
Expand Down
65 changes: 20 additions & 45 deletions packages/cashscript/src/SignatureTemplate.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,40 @@
import { decodePrivateKeyWif, hexToBin, isHex, secp256k1, SigningSerializationFlag } from '@bitauth/libauth';
import { decodePrivateKeyWif, hexToBin, isHex, secp256k1 } from '@bitauth/libauth';
import { hash256, scriptToBytecode } from '@cashscript/utils';
import {
GenerateUnlockingBytecodeOptions,
HashType,
SighashType,
SignatureAlgorithm,
P2PKHUnlocker,
} from './interfaces.js';
import { createSighashPreimage, publicKeyToP2PKHLockingBytecode } from './utils.js';
import { createSighashPreimage, publicKeyToP2PKHLockingBytecode, toSigningSerializationType } from './utils.js';

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

/** The 33-byte compressed public key that corresponds to the template's private key. */
get publicKey(): Uint8Array {
return secp256k1.derivePublicKeyCompressed(this.privateKey) as Uint8Array;
}

/**
* Create a new SignatureTemplate.
*
* @param signer - A 32-byte private key (Uint8Array), a WIF or hex-encoded private key string,
* or any object exposing a `toWIF()` method (e.g. bitcore-lib or bitcoincashjs `ECPair`).
* @param hashtype - Sighash flags to use when signing. Defaults to `SIGHASH_ALL | SIGHASH_UTXOS`.
* @param sighashType - Sighash flags to use when signing. Defaults to `SIGHASH_ALL | SIGHASH_UTXOS`.
* @param signatureAlgorithm - The signature algorithm to use. Defaults to
* `SignatureAlgorithm.SCHNORR`.
*/
constructor(
signer: Keypair | Uint8Array | string,
private hashtype: HashType = HashType.SIGHASH_ALL | HashType.SIGHASH_UTXOS,
private signatureAlgorithm: SignatureAlgorithm = SignatureAlgorithm.SCHNORR,
public readonly sighashType: SighashType = SighashType.SIGHASH_ALL | SighashType.SIGHASH_UTXOS,
public readonly signatureAlgorithm: SignatureAlgorithm = SignatureAlgorithm.SCHNORR,
) {
if (isKeypair(signer)) {
const wif = signer.toWIF();
Expand All @@ -47,24 +52,22 @@ export default class SignatureTemplate {
}

/**
* Sign the provided sighash payload and return the signature concatenated with the hashtype
* Sign the provided sighash payload and return the signature concatenated with the sighash type
* byte, ready to be used as a transaction signature.
*
* @param payload - The 32-byte sighash to sign.
* @param bchForkId - Whether to include the BCH fork id flag in the appended hashtype byte.
* Defaults to `true`.
* @returns The signature bytes followed by the hashtype byte.
* @returns The signature bytes followed by the sighash type byte.
*/
generateSignature(payload: Uint8Array, bchForkId?: boolean): Uint8Array {
generateSignature(payload: Uint8Array): Uint8Array {
const signature = this.signMessageHash(payload);
return Uint8Array.from([...signature, this.getHashType(bchForkId)]);
return Uint8Array.from([...signature, toSigningSerializationType(this.sighashType)]);
}

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

/**
* Get the sighash flags used by this template.
*
* @param bchForkId - Whether to OR in the BCH fork id flag. Defaults to `true`.
* @returns The combined hashtype byte.
*/
getHashType(bchForkId: boolean = true): number {
return bchForkId ? (this.hashtype | SigningSerializationFlag.forkId) : this.hashtype;
}

/**
* @returns The signature algorithm (ECDSA or Schnorr) used by this template.
*/
getSignatureAlgorithm(): SignatureAlgorithm {
return this.signatureAlgorithm;
}

/**
* Derive the compressed public key that corresponds to the template's private key.
*
* @returns The 33-byte compressed public key.
*/
getPublicKey(): Uint8Array {
return secp256k1.derivePublicKeyCompressed(this.privateKey) as Uint8Array;
}

/**
* Build a P2PKH `Unlocker` for the address derived from this template's private key. The
* returned unlocker can be passed directly to `TransactionBuilder.addInput`.
*
* @returns An unlocker that signs the corresponding P2PKH UTXO.
*/
unlockP2PKH(): P2PKHUnlocker {
const publicKey = this.getPublicKey();
const prevOutScript = publicKeyToP2PKHLockingBytecode(publicKey);
const hashtype = this.getHashType();
const prevOutScript = publicKeyToP2PKHLockingBytecode(this.publicKey);

return {
generateLockingBytecode: () => prevOutScript,
generateUnlockingBytecode: ({ transaction, sourceOutputs, inputIndex }: GenerateUnlockingBytecodeOptions) => {
const preimage = createSighashPreimage(transaction, sourceOutputs, inputIndex, prevOutScript, hashtype);
const preimage = createSighashPreimage(transaction, sourceOutputs, inputIndex, prevOutScript, this.sighashType);
const sighash = hash256(preimage);
const signature = this.generateSignature(sighash);
const unlockingBytecode = scriptToBytecode([signature, publicKey]);
const unlockingBytecode = scriptToBytecode([signature, this.publicKey]);
return unlockingBytecode;
},
template: this,
Expand Down
2 changes: 1 addition & 1 deletion packages/cashscript/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ export enum SignatureAlgorithm {
SCHNORR = 0x01,
}

export enum HashType {
export enum SighashType {
SIGHASH_ALL = 0x01,
SIGHASH_NONE = 0x02,
SIGHASH_SINGLE = 0x03,
Expand Down
38 changes: 19 additions & 19 deletions packages/cashscript/src/libauth-template/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AbiFunction, AbiInput, Artifact, formatBitAuthScript, sha256 } from '@cashscript/utils';
import { HashType, LibauthTokenDetails, SignatureAlgorithm, TokenDetails, VmTarget } from '../interfaces.js';
import { LibauthTokenDetails, SighashType, SignatureAlgorithm, TokenDetails, VmTarget } from '../interfaces.js';
import { hexToBin, binToHex, isHex, decodeCashAddress, Input, assertSuccess, decodeAuthenticationInstructions, AuthenticationInstructionPush } from '@bitauth/libauth';
import { EncodedFunctionArgument } from '../Argument.js';
import { zip } from '../utils.js';
Expand Down Expand Up @@ -32,23 +32,23 @@ export const getSignatureAlgorithmName = (signatureAlgorithm: SignatureAlgorithm
return signatureAlgorithmNames[signatureAlgorithm];
};

export const getHashTypeName = (hashType: HashType): string => {
const hashtypeNames = {
[HashType.SIGHASH_ALL]: 'all_outputs',
[HashType.SIGHASH_ALL | HashType.SIGHASH_ANYONECANPAY]: 'all_outputs_single_input',
[HashType.SIGHASH_ALL | HashType.SIGHASH_UTXOS]: 'all_outputs_all_utxos',
[HashType.SIGHASH_ALL | HashType.SIGHASH_ANYONECANPAY | HashType.SIGHASH_UTXOS]: 'all_outputs_single_input_INVALID_all_utxos',
[HashType.SIGHASH_SINGLE]: 'corresponding_output',
[HashType.SIGHASH_SINGLE | HashType.SIGHASH_ANYONECANPAY]: 'corresponding_output_single_input',
[HashType.SIGHASH_SINGLE | HashType.SIGHASH_UTXOS]: 'corresponding_output_all_utxos',
[HashType.SIGHASH_SINGLE | HashType.SIGHASH_ANYONECANPAY | HashType.SIGHASH_UTXOS]: 'corresponding_output_single_input_INVALID_all_utxos',
[HashType.SIGHASH_NONE]: 'no_outputs',
[HashType.SIGHASH_NONE | HashType.SIGHASH_ANYONECANPAY]: 'no_outputs_single_input',
[HashType.SIGHASH_NONE | HashType.SIGHASH_UTXOS]: 'no_outputs_all_utxos',
[HashType.SIGHASH_NONE | HashType.SIGHASH_ANYONECANPAY | HashType.SIGHASH_UTXOS]: 'no_outputs_single_input_INVALID_all_utxos',
export const getSighashTypeName = (sighashType: SighashType): string => {
const sighashTypeNames = {
[SighashType.SIGHASH_ALL]: 'all_outputs',
[SighashType.SIGHASH_ALL | SighashType.SIGHASH_ANYONECANPAY]: 'all_outputs_single_input',
[SighashType.SIGHASH_ALL | SighashType.SIGHASH_UTXOS]: 'all_outputs_all_utxos',
[SighashType.SIGHASH_ALL | SighashType.SIGHASH_ANYONECANPAY | SighashType.SIGHASH_UTXOS]: 'all_outputs_single_input_INVALID_all_utxos',
[SighashType.SIGHASH_SINGLE]: 'corresponding_output',
[SighashType.SIGHASH_SINGLE | SighashType.SIGHASH_ANYONECANPAY]: 'corresponding_output_single_input',
[SighashType.SIGHASH_SINGLE | SighashType.SIGHASH_UTXOS]: 'corresponding_output_all_utxos',
[SighashType.SIGHASH_SINGLE | SighashType.SIGHASH_ANYONECANPAY | SighashType.SIGHASH_UTXOS]: 'corresponding_output_single_input_INVALID_all_utxos',
[SighashType.SIGHASH_NONE]: 'no_outputs',
[SighashType.SIGHASH_NONE | SighashType.SIGHASH_ANYONECANPAY]: 'no_outputs_single_input',
[SighashType.SIGHASH_NONE | SighashType.SIGHASH_UTXOS]: 'no_outputs_all_utxos',
[SighashType.SIGHASH_NONE | SighashType.SIGHASH_ANYONECANPAY | SighashType.SIGHASH_UTXOS]: 'no_outputs_single_input_INVALID_all_utxos',
};

return hashtypeNames[hashType];
return sighashTypeNames[sighashType];
};

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

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

const typeStr = input.type === 'bytes' ? `bytes${arg.length}` : input.type;
Expand Down
11 changes: 9 additions & 2 deletions packages/cashscript/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
bigIntToCompactUint,
NonFungibleTokenCapability,
bigIntToVmNumber,
SigningSerializationFlag,
} from '@bitauth/libauth';
import {
encodeInt,
Expand All @@ -35,6 +36,7 @@ import {
UnlockableUtxo,
LibauthTokenDetails,
ContractType,
SighashType,
} from './interfaces.js';
import { VERSION_SIZE, LOCKTIME_SIZE } from './constants.js';
import {
Expand Down Expand Up @@ -265,15 +267,20 @@ function toBin(output: string): Uint8Array {
return encode(data);
}

// BCH consensus requires the fork id flag on every signing serialization
export function toSigningSerializationType(sighashType: SighashType): number {
return sighashType | SigningSerializationFlag.forkId;
}

export function createSighashPreimage(
transaction: Transaction,
sourceOutputs: LibauthOutput[],
inputIndex: number,
coveredBytecode: Uint8Array,
hashtype: number,
sighashType: SighashType,
): Uint8Array {
const context = { inputIndex, sourceOutputs, transaction };
const signingSerializationType = new Uint8Array([hashtype]);
const signingSerializationType = new Uint8Array([toSigningSerializationType(sighashType)]);

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

Expand Down
18 changes: 6 additions & 12 deletions packages/cashscript/test/SignatureTemplate.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { generateLibauthSourceOutputs } from 'cashscript/dist/utils.js';
import { HashType, MockNetworkProvider, SignatureAlgorithm, SignatureTemplate, TransactionBuilder } from '../src/index.js';
import { MockNetworkProvider, SighashType, SignatureAlgorithm, SignatureTemplate, TransactionBuilder } from '../src/index.js';
import { aliceAddress, alicePriv, alicePub, aliceWif } from './fixture/vars.js';
import { binToHex, decodeTransactionUnsafe, hexToBin } from '@bitauth/libauth';

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

it('should append the correct hash type when fork ID is true', () => {
const signatureTemplate = new SignatureTemplate(alicePriv, HashType.SIGHASH_SINGLE);
const signature = signatureTemplate.generateSignature(hexToBin('0000000000000000000000'), true);
it('should append the configured sighash type, always including the BCH fork ID', () => {
const signatureTemplate = new SignatureTemplate(alicePriv, SighashType.SIGHASH_SINGLE);
const signature = signatureTemplate.generateSignature(hexToBin('0000000000000000000000'));
expect(signature).toEqual(hexToBin('bcac180e17de108003cce026708bd2af54b860dad2626cee157f4ed5abd993b9085d615015f905978adc51e8878226280ddd27d899f086519c0978e53332d79943'));
});

it('should append the correct hash type when fork ID is false', () => {
const signatureTemplate = new SignatureTemplate(alicePriv, HashType.SIGHASH_SINGLE);
const signature = signatureTemplate.generateSignature(hexToBin('0000000000000000000000'), false);
expect(signature).toEqual(hexToBin('bcac180e17de108003cce026708bd2af54b860dad2626cee157f4ed5abd993b9085d615015f905978adc51e8878226280ddd27d899f086519c0978e53332d79903'));
});
});

describe('signMessageHash', () => {
Expand All @@ -62,10 +56,10 @@ describe('SignatureTemplate', () => {
});
});

describe('getPublicKey', () => {
describe('publicKey', () => {
it('should generate a correct public key', () => {
const signatureTemplate = new SignatureTemplate(alicePriv);
expect(signatureTemplate.getPublicKey()).toEqual(alicePub);
expect(signatureTemplate.publicKey).toEqual(alicePub);
});
});

Expand Down
6 changes: 3 additions & 3 deletions packages/cashscript/test/e2e/HodlVault.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
Network,
TransactionBuilder,
SignatureAlgorithm,
HashType,
SighashType,
} from '../../src/index.js';
import {
alicePriv,
Expand Down Expand Up @@ -108,7 +108,7 @@ describe('HodlVault', () => {
const amount = 10000n;
const { utxos, changeAmount } = gatherUtxos(await hodlVault.getUtxos(), { amount, fee: 2000n });

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

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

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

// datasig: unlocker should not throw when given a proper length, but transaction should fail on invalid sig
Expand Down
12 changes: 6 additions & 6 deletions packages/cashscript/test/fixture/libauth-template/fixtures.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Contract, HashType, MockNetworkProvider, SignatureAlgorithm, SignatureTemplate, TransactionBuilder, randomNFT, randomToken, randomUtxo } from '../../../src/index.js';
import { Contract, MockNetworkProvider, SighashType, SignatureAlgorithm, SignatureTemplate, TransactionBuilder, randomNFT, randomToken, randomUtxo } from '../../../src/index.js';
import TransferWithTimeout from '../transfer_with_timeout.artifact.js';
import Mecenas from '../mecenas.artifact.js';
import P2PKH from '../p2pkh.artifact.js';
Expand Down Expand Up @@ -1262,7 +1262,7 @@ export const fixtures: Fixture[] = [
},
},
},
// TODO: Make it work with different hashtypes and signature algorithms
// TODO: Make it work with different sighash types and signature algorithms
// {
// name: 'P2PKH (sending NFTs)',
// transaction: (() => {
Expand All @@ -1272,11 +1272,11 @@ export const fixtures: Fixture[] = [
// const to = contract.address;
// const amount = 1000n;

// const hashtype = HashType.SIGHASH_SINGLE | HashType.SIGHASH_ANYONECANPAY;
// const sighashType = SighashType.SIGHASH_SINGLE | SighashType.SIGHASH_ANYONECANPAY;
// const signatureAlgorithm = SignatureAlgorithm.ECDSA;

// const tx = contract.functions
// .spend(alicePub, new SignatureTemplate(alicePriv, hashtype, signatureAlgorithm))
// .spend(alicePub, new SignatureTemplate(alicePriv, sighashType, signatureAlgorithm))
// .to(to, amount);

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

const aliceDefaultTemplate = new SignatureTemplate(alicePriv);
const aliceCustomTemplate = new SignatureTemplate(alicePriv, HashType.SIGHASH_NONE, SignatureAlgorithm.ECDSA);
const bobCustomTemplate = new SignatureTemplate(bobPriv, HashType.SIGHASH_ALL, SignatureAlgorithm.ECDSA);
const aliceCustomTemplate = new SignatureTemplate(alicePriv, SighashType.SIGHASH_NONE, SignatureAlgorithm.ECDSA);
const bobCustomTemplate = new SignatureTemplate(bobPriv, SighashType.SIGHASH_ALL, SignatureAlgorithm.ECDSA);

const tx = new TransactionBuilder({ provider })
.addInput(p2pkhUtxo, aliceDefaultTemplate.unlockP2PKH())
Expand Down
Loading
Loading