import { schnorr, secp256k1 } from "@noble/curves/secp256k1"; import { hmac } from "@noble/hashes/hmac"; import { bytesToHex, equalBytes, hexToBytes } from "@noble/curves/utils"; import { sha256 } from "@noble/hashes/sha2"; import { HDKey } from "@scure/bip32"; import { generateMnemonic, mnemonicToSeed } from "@scure/bip39"; import { wordlist } from "@scure/bip39/wordlists/english"; import { type Transaction } from "@scure/btc-signer"; import { SparkError, SparkValidationError } from "../errors/index.js"; import { getSparkFrost } from "../spark-bindings/spark-bindings.js"; import { type AggregateFrostBindingParams, type IKeyPackage, type SignFrostBindingParams, } from "../spark-bindings/types.js"; import { subtractPrivateKeys } from "../utils/keys.js"; import { type VerifiableSecretShare } from "../utils/secret-sharing.js"; import { getRandomSigningNonce, getSigningCommitmentFromNonce, } from "../utils/signing.js"; import { KeyDerivationType, type SigningCommitmentWithOptionalNonce, type AggregateFrostParams, type DerivedHDKey, type KeyDerivation, type KeyPair, type SignFrostParams, type SigningCommitment, type SigningNonce, type SplitSecretWithProofsParams, type SubtractSplitAndEncryptParams, type SubtractSplitAndEncryptResult, } from "./types.js"; interface SparkKeysGenerator { deriveKeysFromSeed( seed: Uint8Array, accountNumber: number, ): Promise<{ identityKey: KeyPair; signingHDKey: DerivedHDKey; depositKey: KeyPair; staticDepositHDKey: DerivedHDKey; HTLCPreimageHDKey: DerivedHDKey; }>; } const HARDENED_OFFSET = 0x80000000; // 2^31 class DefaultSparkKeysGenerator implements SparkKeysGenerator { deriveKeysFromSeed( seed: Uint8Array, accountNumber: number, ): Promise<{ identityKey: KeyPair; signingHDKey: DerivedHDKey; depositKey: KeyPair; staticDepositHDKey: DerivedHDKey; HTLCPreimageHDKey: DerivedHDKey; }> { return new Promise((resolve) => { const hdkey = HDKey.fromMasterSeed(seed); if (!hdkey.privateKey || !hdkey.publicKey) { throw new SparkValidationError("Failed to derive keys from seed", { field: "hdkey", value: seed, }); } const identityKey = hdkey.derive(`m/8797555'/${accountNumber}'/0'`); const signingKey = hdkey.derive(`m/8797555'/${accountNumber}'/1'`); const depositKey = hdkey.derive(`m/8797555'/${accountNumber}'/2'`); const staticDepositKey = hdkey.derive(`m/8797555'/${accountNumber}'/3'`); const htlcPreimageKey = hdkey.derive(`m/8797555'/${accountNumber}'/4'`); if ( !identityKey.privateKey || !depositKey.privateKey || !signingKey.privateKey || !identityKey.publicKey || !depositKey.publicKey || !signingKey.publicKey || !staticDepositKey.privateKey || !staticDepositKey.publicKey || !htlcPreimageKey.privateKey || !htlcPreimageKey.publicKey ) { throw new SparkValidationError( "Failed to derive all required keys from seed", { field: "derivedKeys", }, ); } resolve({ identityKey: { privateKey: identityKey.privateKey, publicKey: identityKey.publicKey, }, signingHDKey: { hdKey: signingKey, privateKey: signingKey.privateKey, publicKey: signingKey.publicKey, }, depositKey: { privateKey: depositKey.privateKey, publicKey: depositKey.publicKey, }, staticDepositHDKey: { hdKey: staticDepositKey, privateKey: staticDepositKey.privateKey, publicKey: staticDepositKey.publicKey, }, HTLCPreimageHDKey: { hdKey: htlcPreimageKey, privateKey: htlcPreimageKey.privateKey, publicKey: htlcPreimageKey.publicKey, }, }); }); } } interface SparkSigner { getIdentityPublicKey(): Promise; getDepositSigningKey(): Promise; getStaticDepositSigningKey(idx: number): Promise; getStaticDepositSecretKey(idx: number): Promise; generateMnemonic(): Promise; mnemonicToSeed(mnemonic: string): Promise; signSchnorrWithIdentityKey(message: Uint8Array): Promise; signFrost(params: SignFrostParams): Promise; aggregateFrost(params: AggregateFrostParams): Promise; decryptEcies(ciphertext: Uint8Array): Promise; getRandomSigningCommitment(): Promise; getDepositSigningKey(): Promise; getNonceForSelfCommitment( selfCommitment: SigningCommitmentWithOptionalNonce, ): SigningNonce | undefined; createSparkWalletFromSeed( seed: Uint8Array | string, accountNumber?: number, ): Promise; getPublicKeyFromDerivation( keyDerivation?: KeyDerivation, ): Promise; subtractPrivateKeysGivenDerivationPaths( first: string, second: string, ): Promise; subtractAndSplitSecretWithProofsGivenDerivations( params: Omit & { first: KeyDerivation; second?: KeyDerivation | undefined; }, ): Promise; subtractSplitAndEncrypt( params: SubtractSplitAndEncryptParams, ): Promise; splitSecretWithProofs( params: SplitSecretWithProofsParams, ): Promise; signMessageWithIdentityKey( message: Uint8Array, /* If compact is true, the signature should be in ecdsa compact format else it should be in DER format */ compact?: boolean, ): Promise; validateMessageWithIdentityKey( message: Uint8Array, signature: Uint8Array, ): Promise; signTransactionIndex( tx: Transaction, index: number, publicKey: Uint8Array, ): void; htlcHMAC(transferID: string): Promise; } type SparkSignerConstructorParams = { sparkKeysGenerator?: SparkKeysGenerator; }; class DefaultSparkSigner implements SparkSigner { private identityKey: KeyPair | null = null; private signingKey: HDKey | null = null; private depositKey: KeyPair | null = null; private staticDepositKey: HDKey | null = null; private htlcPreimageKey: HDKey | null = null; private readonly keysGenerator: SparkKeysGenerator; protected commitmentToNonceMap: Map = new Map(); constructor({ sparkKeysGenerator }: SparkSignerConstructorParams = {}) { this.keysGenerator = sparkKeysGenerator ?? new DefaultSparkKeysGenerator(); } private deriveSigningKey(hash: Uint8Array): Uint8Array { if (!this.signingKey) { throw new SparkValidationError("Private key not initialized", { field: "signingKey", }); } const view = new DataView(hash.buffer); const amount = (view.getUint32(0, false) % HARDENED_OFFSET) + HARDENED_OFFSET; const newPrivateKey = this.signingKey?.deriveChild(amount).privateKey; if (!newPrivateKey) { throw new SparkValidationError("Failed to recover signing key", { field: "privateKey", }); } return newPrivateKey; } private async decryptEciesToPrivateKey( ciphertext: Uint8Array, ): Promise { if (!this.identityKey?.privateKey) { throw new SparkError("identityKey not initialized"); } const sparkFrost = getSparkFrost(); const privateKey = await sparkFrost.decryptEcies( ciphertext, this.identityKey.privateKey, ); return privateKey; } protected async getSigningPrivateKeyFromDerivation( keyDerivation: KeyDerivation, ): Promise { switch (keyDerivation.type) { case KeyDerivationType.LEAF: return this.deriveSigningKey(sha256(keyDerivation.path)); case KeyDerivationType.DEPOSIT: return this.depositKey?.privateKey ?? new Uint8Array(); case KeyDerivationType.STATIC_DEPOSIT: return this.getStaticDepositSecretKey(keyDerivation.path); case KeyDerivationType.ECIES: return this.decryptEciesToPrivateKey(keyDerivation.path); case KeyDerivationType.RANDOM: return secp256k1.utils.randomPrivateKey(); } } signSchnorrWithIdentityKey(message: Uint8Array): Promise { return new Promise((resolve) => { if (!this.identityKey?.privateKey) { throw new SparkValidationError("Private key not set", { field: "identityKey", }); } resolve(schnorr.sign(message, this.identityKey.privateKey)); }); } getIdentityPublicKey(): Promise { return new Promise((resolve) => { if (!this.identityKey?.publicKey) { throw new SparkValidationError("Private key is not set", { field: "identityKey", }); } resolve(this.identityKey.publicKey); }); } getDepositSigningKey(): Promise { return new Promise((resolve) => { if (!this.depositKey?.publicKey) { throw new SparkValidationError("Deposit key is not set", { field: "depositKey", }); } resolve(this.depositKey.publicKey); }); } async getStaticDepositSigningKey(idx: number): Promise { const staticDepositKey = await this.getStaticDepositSecretKey(idx); return secp256k1.getPublicKey(staticDepositKey); } getStaticDepositSecretKey(idx: number): Promise { return new Promise((resolve) => { if (!this.staticDepositKey) { throw new SparkValidationError("Static deposit key is not set", { field: "staticDepositKey", }); } const staticDepositKey = this.staticDepositKey.deriveChild( HARDENED_OFFSET + idx, ); if (!staticDepositKey?.privateKey) { throw new SparkValidationError("Static deposit key is not set", { field: "staticDepositKey", }); } resolve(staticDepositKey.privateKey); }); } generateMnemonic(): Promise { return new Promise((resolve) => { resolve(generateMnemonic(wordlist)); }); } async mnemonicToSeed(mnemonic: string): Promise { return await mnemonicToSeed(mnemonic); } async getPublicKeyFromDerivation( keyDerivation: KeyDerivation, ): Promise { const privateKey = await this.getSigningPrivateKeyFromDerivation(keyDerivation); return secp256k1.getPublicKey(privateKey); } subtractPrivateKeysGivenDerivationPaths( first: string, second: string, ): Promise { return new Promise((resolve) => { const firstPrivateKey = this.deriveSigningKey(sha256(first)); const secondPrivateKey = this.deriveSigningKey(sha256(second)); const resultPrivKey = subtractPrivateKeys( firstPrivateKey, secondPrivateKey, ); resolve(secp256k1.getPublicKey(resultPrivKey)); }); } async subtractAndSplitSecretWithProofsGivenDerivations({ first, second, curveOrder, threshold, numShares, }: Omit & { first: KeyDerivation; second: KeyDerivation; }): Promise { const firstPrivateKey = await this.getSigningPrivateKeyFromDerivation(first); const secondPrivateKey = await this.getSigningPrivateKeyFromDerivation(second); const resultPrivKey = subtractPrivateKeys( firstPrivateKey, secondPrivateKey, ); return await this.splitSecretWithProofs({ secret: resultPrivKey, curveOrder, threshold, numShares, }); } async subtractSplitAndEncrypt({ first, second, curveOrder, threshold, numShares, receiverPublicKey, }: Omit & { first: KeyDerivation; second: KeyDerivation; receiverPublicKey: Uint8Array; }): Promise { const firstPrivateKey = await this.getSigningPrivateKeyFromDerivation(first); const secondPrivateKey = await this.getSigningPrivateKeyFromDerivation(second); const resultPrivKey = subtractPrivateKeys( firstPrivateKey, secondPrivateKey, ); const sparkFrost = getSparkFrost(); return { shares: await this.splitSecretWithProofs({ secret: resultPrivKey, curveOrder, threshold, numShares, }), secretCipher: await sparkFrost.encryptEcies( secondPrivateKey, receiverPublicKey, ), }; } async splitSecretWithProofs({ secret, curveOrder, threshold, numShares, }: SplitSecretWithProofsParams): Promise { const sparkFrost = getSparkFrost(); return sparkFrost.splitSecretWithProofs(secret, threshold, numShares); } getNonceForSelfCommitment( selfCommitment: SigningCommitmentWithOptionalNonce, ) { const nonce = this.commitmentToNonceMap.get(selfCommitment.commitment); return nonce; } async buildSignFrostParams({ message, keyDerivation, publicKey, verifyingKey, selfCommitment, statechainCommitments, adaptorPubKey, }: SignFrostParams): Promise { const signingPrivateKey = await this.getSigningPrivateKeyFromDerivation(keyDerivation); if (!signingPrivateKey) { throw new SparkValidationError("Private key not found for public key", { field: "privateKey", }); } const nonce = this.getNonceForSelfCommitment(selfCommitment); if (!nonce) { throw new SparkValidationError("Nonce not found for commitment", { field: "nonce", }); } const keyPackage: IKeyPackage = { secretKey: signingPrivateKey, publicKey: publicKey, verifyingKey: verifyingKey, }; return { message, keyPackage, nonce, selfCommitment: selfCommitment.commitment, statechainCommitments, adaptorPubKey, }; } buildAggregateFrostParams( params: AggregateFrostParams, ): Promise { return new Promise((resolve) => { const { message, publicKey, verifyingKey, selfCommitment, statechainCommitments, adaptorPubKey, selfSignature, statechainSignatures, statechainPublicKeys, } = params; resolve({ message, statechainSignatures, statechainPublicKeys, verifyingKey, statechainCommitments, selfCommitment: selfCommitment.commitment, selfPublicKey: publicKey, selfSignature, adaptorPubKey, }); }); } async signFrost({ message, keyDerivation, publicKey, verifyingKey, selfCommitment, statechainCommitments, adaptorPubKey, }: SignFrostParams): Promise { const signFrostParams = await this.buildSignFrostParams({ message, keyDerivation, publicKey, verifyingKey, selfCommitment, statechainCommitments, adaptorPubKey, }); const sparkFrost = getSparkFrost(); return sparkFrost.signFrost(signFrostParams); } async aggregateFrost({ message, publicKey, verifyingKey, selfCommitment, statechainCommitments, adaptorPubKey, selfSignature, statechainSignatures, statechainPublicKeys, }: AggregateFrostParams): Promise { const aggregateFrostParams = await this.buildAggregateFrostParams({ message, publicKey, verifyingKey, selfCommitment, statechainCommitments, adaptorPubKey, selfSignature, statechainSignatures, statechainPublicKeys, }); const sparkFrost = getSparkFrost(); return sparkFrost.aggregateFrost(aggregateFrostParams); } async createSparkWalletFromSeed( seed: Uint8Array | string, accountNumber?: number, ): Promise { if (typeof seed === "string") { seed = hexToBytes(seed); } const { identityKey, signingHDKey: signingKey, depositKey, staticDepositHDKey: staticDepositKey, HTLCPreimageHDKey: htlcPreimageKey, } = await this.keysGenerator.deriveKeysFromSeed(seed, accountNumber ?? 0); this.identityKey = identityKey; this.depositKey = depositKey; this.signingKey = signingKey.hdKey; this.staticDepositKey = staticDepositKey.hdKey; this.htlcPreimageKey = htlcPreimageKey.hdKey; return bytesToHex(identityKey.publicKey); } signMessageWithIdentityKey( message: Uint8Array, compact?: boolean, ): Promise { return new Promise((resolve) => { if (!this.identityKey?.privateKey) { throw new SparkError("Identity key not initialized", { configKey: "identityKey", }); } const signature = secp256k1.sign(message, this.identityKey.privateKey); if (compact) { resolve(signature.toCompactRawBytes()); return; } resolve(signature.toDERRawBytes()); }); } async decryptEcies(ciphertext: Uint8Array): Promise { if (!this.identityKey?.privateKey) { throw new SparkError("identityKey not initialized"); } const sparkFrost = getSparkFrost(); const privateKey = await sparkFrost.decryptEcies( ciphertext, this.identityKey.privateKey, ); const publicKey = secp256k1.getPublicKey(privateKey); return publicKey; } getRandomSigningCommitment(): Promise { return new Promise((resolve) => { const nonce = getRandomSigningNonce(); const commitment = getSigningCommitmentFromNonce(nonce); this.commitmentToNonceMap.set(commitment, nonce); resolve({ commitment, }); }); } validateMessageWithIdentityKey( message: Uint8Array, signature: Uint8Array, ): Promise { return new Promise((resolve) => { if (!this.identityKey?.publicKey) { throw new SparkError("identityKey not initialized"); } resolve(secp256k1.verify(signature, message, this.identityKey.publicKey)); }); } signTransactionIndex( tx: Transaction, index: number, publicKey: Uint8Array, ): void { let privateKey: Uint8Array | undefined | null; if ( equalBytes(publicKey, this.identityKey?.publicKey ?? new Uint8Array()) ) { privateKey = this.identityKey?.privateKey; } else if ( equalBytes(publicKey, this.depositKey?.publicKey ?? new Uint8Array()) ) { privateKey = this.depositKey?.privateKey; } if (!privateKey) { throw new SparkValidationError("Private key not found for public key", { field: "privateKey", value: bytesToHex(publicKey), }); } tx.signIdx(privateKey, index); } htlcHMAC(transferID: string): Promise { return new Promise((resolve) => { if (!this.htlcPreimageKey?.privateKey) { throw new SparkError("HTLC preimage key not initialized", { configKey: "htlcPreimageKey", }); } resolve(hmac(sha256, this.htlcPreimageKey.privateKey, transferID)); }); } } /** * StatelessSparkSigner is different from DefaultSparkSigner in that it does not store * nonces in internal state. StatelessSparkSigner should only be used in a secure environment. * * @extends DefaultSparkSigner */ class UnsafeStatelessSparkSigner extends DefaultSparkSigner { getNonceForSelfCommitment( selfCommitment: SigningCommitmentWithOptionalNonce, ): SigningNonce | undefined { return selfCommitment.nonce; } getRandomSigningCommitment(): Promise { return new Promise((resolve) => { const nonce = getRandomSigningNonce(); const commitment = getSigningCommitmentFromNonce(nonce); resolve({ commitment, nonce, }); }); } } export { DefaultSparkSigner, UnsafeStatelessSparkSigner, type SparkSigner };