1212 * https://github.com/kaspanet/rusty-kaspa/blob/master/consensus/core/src/hashing/sighash.rs
1313 */
1414import { blake2b } from 'blakejs' ;
15+ import { createHash } from 'crypto' ;
1516import { KaspaTransactionData , KaspaUtxoInput , KaspaTransactionOutput } from './iface' ;
16-
17- // SigHash type flags
18- export const SIGHASH_ALL = 0x01 ;
19- export const SIGHASH_NONE = 0x02 ;
20- export const SIGHASH_SINGLE = 0x04 ;
21- export const SIGHASH_ANYONECANPAY = 0x80 ;
22-
23- // Script constants
24- export const OP_CHECKSIG_SCHNORR = 0xab ; // Kaspa Schnorr checksig opcode
25- export const SCRIPT_PUBLIC_KEY_VERSION = 0 ; // Standard P2PK version
17+ import {
18+ SIGHASH_ALL ,
19+ SIGHASH_NONE ,
20+ SIGHASH_SINGLE ,
21+ SIGHASH_ANYONECANPAY ,
22+ OP_CHECKSIG_SCHNORR ,
23+ OP_CHECKSIG_ECDSA ,
24+ KaspaScriptType ,
25+ } from './constants' ;
2626
2727/**
2828 * The Blake2b key used for ALL sighash operations in Kaspa.
@@ -33,15 +33,46 @@ export const SCRIPT_PUBLIC_KEY_VERSION = 0; // Standard P2PK version
3333 */
3434const SIGNING_HASH_KEY = Buffer . from ( 'TransactionSigningHash' , 'ascii' ) ;
3535
36- /**
37- * Keyed Blake2b-256: blake2b(data, key="TransactionSigningHash", outlen=32).
38- * Used for every intermediate hash and the final sighash.
39- */
4036function kblake2b ( data : Buffer ) : Buffer {
4137 return Buffer . from ( blake2b ( data , SIGNING_HASH_KEY , 32 ) ) ;
4238}
4339
44- // ─── Intermediate hash helpers ────────────────────────────────────────────────
40+ /**
41+ * Build a Kaspa P2PK scriptPublicKey.
42+ *
43+ * @param pubKey - For SCHNORR: 32-byte x-only key.
44+ * For ECDSA: 33-byte compressed key.
45+ * @param type - Address type (default: SCHNORR / v0).
46+ *
47+ * Resulting script formats (per rusty-kaspa crypto/txscript/src/standard.rs):
48+ * SCHNORR (v0): OP_DATA_32(0x20) || xOnlyPubKey(32B) || OpCheckSig(0xAC)
49+ * ECDSA (v1): OP_DATA_33(0x21) || compressedPubKey(33B) || OpCheckSigECDSA(0xAB)
50+ */
51+ export function buildP2PKScriptPublicKey ( pubKey : Buffer , type : KaspaScriptType = KaspaScriptType . SCHNORR ) : Buffer {
52+ if ( type === KaspaScriptType . SCHNORR ) {
53+ if ( pubKey . length !== 32 ) {
54+ throw new Error ( `SCHNORR script expects 32-byte x-only pubkey, got ${ pubKey . length } ` ) ;
55+ }
56+ return Buffer . concat ( [ Buffer . from ( [ 0x20 ] ) , pubKey , Buffer . from ( [ OP_CHECKSIG_SCHNORR ] ) ] ) ;
57+ } else {
58+ if ( pubKey . length !== 33 ) {
59+ throw new Error ( `ECDSA script expects 33-byte compressed pubkey, got ${ pubKey . length } ` ) ;
60+ }
61+ return Buffer . concat ( [ Buffer . from ( [ 0x21 ] ) , pubKey , Buffer . from ( [ OP_CHECKSIG_ECDSA ] ) ] ) ;
62+ }
63+ }
64+
65+ /**
66+ * Derive x-only public key from 33-byte compressed public key.
67+ */
68+ export function compressedToXOnly ( compressedPubKey : Buffer ) : Buffer {
69+ if ( compressedPubKey . length !== 33 ) {
70+ throw new Error ( `Expected 33-byte compressed pubkey, got ${ compressedPubKey . length } ` ) ;
71+ }
72+ return compressedPubKey . slice ( 1 ) ; // drop the 02/03 prefix byte
73+ }
74+
75+ // --- Intermediate hash helpers ---
4576
4677function hashPreviousOutputs ( inputs : KaspaUtxoInput [ ] ) : Buffer {
4778 const parts = inputs . map ( ( inp ) => {
@@ -115,29 +146,6 @@ function hashPayload(tx: KaspaTransactionData): Buffer {
115146 return kblake2b ( Buffer . concat ( [ lenBuf , payloadBytes ] ) ) ;
116147}
117148
118- // ─── Public API ───────────────────────────────────────────────────────────────
119-
120- /**
121- * Build P2PK Schnorr scriptPublicKey from a 32-byte x-only public key.
122- * Format: OP_DATA_32(0x20) + xOnlyPubKey(32 bytes) + OP_CHECKSIG_SCHNORR(0xAB)
123- */
124- export function buildP2PKScriptPublicKey ( xOnlyPubKey : Buffer ) : Buffer {
125- if ( xOnlyPubKey . length !== 32 ) {
126- throw new Error ( `Expected 32-byte x-only pubkey, got ${ xOnlyPubKey . length } ` ) ;
127- }
128- return Buffer . concat ( [ Buffer . from ( [ 0x20 ] ) , xOnlyPubKey , Buffer . from ( [ OP_CHECKSIG_SCHNORR ] ) ] ) ;
129- }
130-
131- /**
132- * Derive x-only public key from 33-byte compressed public key.
133- */
134- export function compressedToXOnly ( compressedPubKey : Buffer ) : Buffer {
135- if ( compressedPubKey . length !== 33 ) {
136- throw new Error ( `Expected 33-byte compressed pubkey, got ${ compressedPubKey . length } ` ) ;
137- }
138- return compressedPubKey . slice ( 1 ) ;
139- }
140-
141149/**
142150 * Compute the Kaspa Schnorr sighash for a specific input.
143151 *
@@ -239,3 +247,34 @@ export function computeKaspaSigningHash(
239247
240248 return kblake2b ( preimage . slice ( 0 , offset ) ) ;
241249}
250+
251+ /**
252+ * SHA-256 domain separator for ECDSA signing.
253+ * Matches TransactionSigningHashECDSA in rusty-kaspa/crypto/hashes/src/hashers.rs:
254+ * sha256_hasher! { struct TransactionSigningHashECDSA => "TransactionSigningHashECDSA" }
255+ * The hasher is seeded with SHA256("TransactionSigningHashECDSA") before any data.
256+ */
257+ const ECDSA_DOMAIN_SEP : Buffer = Buffer . from ( createHash ( 'sha256' ) . update ( 'TransactionSigningHashECDSA' ) . digest ( ) ) ;
258+
259+ /**
260+ * Compute the Kaspa ECDSA sighash for a specific input.
261+ *
262+ * ECDSA signing uses a two-step hash defined in rusty-kaspa sighash.rs:
263+ * 1. schnorr_hash = blake2b256_keyed("TransactionSigningHash", preimage)
264+ * 2. ecdsa_hash = SHA256( SHA256("TransactionSigningHashECDSA") || schnorr_hash )
265+ *
266+ * This differs from the Schnorr hash — using the Schnorr hash for ECDSA signing
267+ * will produce a locally-valid signature that the network always rejects.
268+ *
269+ * @param tx Full transaction data with UTXO amount + scriptPublicKey on inputs
270+ * @param inputIndex 0-based index of the input being signed
271+ * @param sigHashType SigHash type flags (SIGHASH_ALL = 0x01)
272+ */
273+ export function computeKaspaEcdsaSigningHash (
274+ tx : KaspaTransactionData ,
275+ inputIndex : number ,
276+ sigHashType : number = SIGHASH_ALL
277+ ) : Buffer {
278+ const schnorrHash = computeKaspaSigningHash ( tx , inputIndex , sigHashType ) ;
279+ return Buffer . from ( createHash ( 'sha256' ) . update ( ECDSA_DOMAIN_SEP ) . update ( schnorrHash ) . digest ( ) ) ;
280+ }
0 commit comments