The Spark SDK provides the SparkSigner interface to enable flexible implementation of signing operations. This abstraction allows you to customize how cryptographic operations are performed, enabling support for secure enclaves, hardware wallets, remote signing services, and other specialized key management systems.
The SDK includes DefaultSparkSigner which handles standard single-signature operations and stores nonces internally for security. For server-side enclave integrations, UnsafeStatelessSparkSigner is available.
Core Concepts
Key Types
Spark wallets derive 5 key types from a master seed using BIP32:
Path Key Type Purpose m/8797555'/n'/0'Identity Key Primary wallet identifier and authentication m/8797555'/n'/1'Signing HD Key Base key for leaf key derivation m/8797555'/n'/2'Deposit Key Receiving L1 Bitcoin deposits m/8797555'/n'/3'Static Deposit HD Key Reusable deposit addresses m/8797555'/n'/4'HTLC Preimage HD Key Lightning HTLC operations
The KeyDerivation System
The signer uses a discriminated union type to specify how to derive or retrieve a private key for signing operations:
enum KeyDerivationType {
LEAF = "leaf" , // Derive from signing HD key using sha256(path)
DEPOSIT = "deposit" , // Use the deposit key directly
STATIC_DEPOSIT = "static_deposit" , // Use static deposit key at index
ECIES = "ecies" , // Decrypt private key from ciphertext
RANDOM = "random" , // Generate a random key (for adaptor signatures)
}
type KeyDerivation =
| { type : KeyDerivationType . LEAF ; path : string }
| { type : KeyDerivationType . DEPOSIT }
| { type : KeyDerivationType . RANDOM }
| { type : KeyDerivationType . STATIC_DEPOSIT ; path : number }
| { type : KeyDerivationType . ECIES ; path : Uint8Array };
This abstraction is used throughout the signer interface, particularly in signFrost() and getPublicKeyFromDerivation().
Security Model
All private keys are derived from a master seed using BIP32 hierarchical deterministic key derivation
Private keys never leave the signer. Only signatures and public keys are returned.
DefaultSparkSigner stores nonces internally to prevent reuse attacks
For enclave integrations, UnsafeStatelessSparkSigner exposes nonces externally
Implementations
DefaultSparkSigner
The recommended implementation for client-side applications. It stores signing nonces internally to prevent reuse attacks.
import { DefaultSparkSigner } from "@buildonspark/spark-sdk" ;
const signer = new DefaultSparkSigner ();
const mnemonic = await signer . generateMnemonic ();
const seed = await signer . mnemonicToSeed ( mnemonic );
await signer . createSparkWalletFromSeed ( seed , 0 );
UnsafeStatelessSparkSigner
For server-side enclave integrations where nonces need to be managed externally. This signer returns nonces in getRandomSigningCommitment() instead of storing them internally.
import { UnsafeStatelessSparkSigner } from "@buildonspark/spark-sdk" ;
// Only use in secure server environments
const signer = new UnsafeStatelessSparkSigner ();
UnsafeStatelessSparkSigner exposes nonces externally. Only use this in secure server environments where you can properly protect nonce data.
Custom Signer Implementation
You can extend DefaultSparkSigner to implement custom signing logic, such as forwarding requests to a secure enclave:
import { DefaultSparkSigner , SignFrostParams } from "@buildonspark/spark-sdk" ;
class EnclaveSigner extends DefaultSparkSigner {
private enclave : EnclaveClient ;
constructor ( enclave : EnclaveClient ) {
super ();
this . enclave = enclave ;
}
async signFrost ( params : SignFrostParams ) : Promise < Uint8Array > {
// Forward signing request to secure enclave
return this . enclave . signFrost ( params );
}
async createSparkWalletFromSeed (
seed : Uint8Array | string ,
accountNumber ?: number
) : Promise < string > {
// Initialize keys in enclave
return this . enclave . initializeWallet ( seed , accountNumber );
}
}
// Use with SparkWallet
const { wallet } = await SparkWallet . initialize ({
signer: new EnclaveSigner ( myEnclave ),
options: { network: "MAINNET" }
});
Custom Key Derivation Paths
For non-standard derivation paths, use DerivationPathKeysGenerator:
import { DefaultSparkSigner , DerivationPathKeysGenerator } from "@buildonspark/spark-sdk" ;
// Use ? as placeholder for account number
const customGenerator = new DerivationPathKeysGenerator ( "m/44'/0'/?'" );
const signer = new DefaultSparkSigner ({
sparkKeysGenerator: customGenerator
});
Wallet Initialization
Generate Mnemonic
generateMnemonic()
Generates a new BIP39 mnemonic phrase for wallet creation.
const mnemonic = await signer . generateMnemonic ();
console . log ( mnemonic ); // "abandon ability able about above absent..."
A 12-word BIP39 mnemonic phrase
Convert Mnemonic to Seed
mnemonicToSeed(mnemonic)
Converts a BIP39 mnemonic phrase to a cryptographic seed.
const seed = await signer . mnemonicToSeed ( mnemonic );
console . log ( "Seed length:" , seed . length ); // 64 bytes
Valid BIP39 mnemonic phrase
seed
Promise<Uint8Array>
required
64-byte seed derived from the mnemonic
Initialize from Seed
createSparkWalletFromSeed(seed, accountNumber?)
Initializes the signer with a master seed and derives all necessary keys.
const seed = await signer . mnemonicToSeed ( mnemonic );
const identityPubKey = await signer . createSparkWalletFromSeed ( seed , 0 );
console . log ( "Identity public key:" , identityPubKey );
seed
Uint8Array | string
required
Master seed as bytes or hex string
Account index for key derivation. Defaults to 0 on REGTEST, 1 on MAINNET for backwards compatibility.
Hex-encoded identity public key
Key Management
Get Identity Public Key
getIdentityPublicKey()
Retrieves the wallet’s identity public key.
const identityKey = await signer . getIdentityPublicKey ();
console . log ( "Identity key:" , identityKey );
identityKey
Promise<Uint8Array>
required
The identity public key (33 bytes, compressed)
Get Deposit Signing Key
getDepositSigningKey()
Retrieves the deposit signing public key used for L1 Bitcoin deposits.
const depositKey = await signer . getDepositSigningKey ();
console . log ( "Deposit signing key:" , depositKey );
depositKey
Promise<Uint8Array>
required
The deposit signing public key
Get Static Deposit Signing Key
getStaticDepositSigningKey(idx)
Retrieves a static deposit signing public key by index.
const signingKey = await signer . getStaticDepositSigningKey ( 0 );
console . log ( "Static deposit signing key:" , signingKey );
Index for the static deposit key
signingKey
Promise<Uint8Array>
required
The static deposit signing public key
Get Static Deposit Secret Key
getStaticDepositSecretKey(idx)
Retrieves a static deposit private key by index. Used when the private key needs to be shared with the SSP for static deposit flows.
const secretKey = await signer . getStaticDepositSecretKey ( 0 );
Index for the static deposit key
secretKey
Promise<Uint8Array>
required
The static deposit private key
Get Public Key from Derivation
getPublicKeyFromDerivation(keyDerivation)
Derives a public key based on a KeyDerivation specification.
import { KeyDerivationType } from "@buildonspark/spark-sdk" ;
// Get public key for a leaf
const leafPubKey = await signer . getPublicKeyFromDerivation ({
type: KeyDerivationType . LEAF ,
path: "leaf-123"
});
// Get deposit public key
const depositPubKey = await signer . getPublicKeyFromDerivation ({
type: KeyDerivationType . DEPOSIT
});
Specifies how to derive the key (LEAF, DEPOSIT, STATIC_DEPOSIT, ECIES, or RANDOM)
publicKey
Promise<Uint8Array>
required
The derived public key
Digital Signatures
Sign with Identity Key
signMessageWithIdentityKey(message, compact?)
Signs a message using the wallet’s identity key with ECDSA.
const message = new TextEncoder (). encode ( "Hello, Spark!" );
const signature = await signer . signMessageWithIdentityKey ( message );
console . log ( "Signature:" , signature );
// With compact format
const compactSignature = await signer . signMessageWithIdentityKey ( message , true );
Use compact signature format instead of DER
signature
Promise<Uint8Array>
required
ECDSA signature (DER or compact format)
Validate Signature
validateMessageWithIdentityKey(message, signature)
Validates an ECDSA signature against the identity key.
const message = new TextEncoder (). encode ( "Hello, Spark!" );
const signature = await signer . signMessageWithIdentityKey ( message );
const isValid = await signer . validateMessageWithIdentityKey ( message , signature );
console . log ( "Signature valid:" , isValid );
True if signature is valid
Sign with Schnorr (Identity Key)
signSchnorrWithIdentityKey(message)
Creates a Schnorr signature using the identity key.
const message = new TextEncoder (). encode ( "Hello, Spark!" );
const schnorrSignature = await signer . signSchnorrWithIdentityKey ( message );
console . log ( "Schnorr signature:" , schnorrSignature );
schnorrSignature
Promise<Uint8Array>
required
Schnorr signature (64 bytes)
Sign Transaction Index
signTransactionIndex(tx, index, publicKey)
Signs a specific input of a Bitcoin transaction. The method looks up the private key based on the provided public key (must be either identity or deposit key).
import { Transaction } from "@scure/btc-signer" ;
const tx = new Transaction ();
// ... build transaction ...
const identityKey = await signer . getIdentityPublicKey ();
signer . signTransactionIndex ( tx , 0 , identityKey );
The Bitcoin transaction to sign (from @scure/btc-signer)
Public key identifying which private key to use
FROST Protocol (Threshold Signatures)
Spark uses FROST (Flexible Round-Optimized Schnorr Threshold) signatures for collaborative signing between users and Signing Operators.
Get Random Signing Commitment
getRandomSigningCommitment()
Generates a random signing commitment for FROST protocol. In DefaultSparkSigner, the nonce is stored internally. In UnsafeStatelessSparkSigner, the nonce is returned in the response.
const commitment = await signer . getRandomSigningCommitment ();
console . log ( "Commitment:" , commitment . commitment );
// commitment.nonce is only present in UnsafeStatelessSparkSigner
commitment
Promise<SigningCommitmentWithOptionalNonce>
required
Object containing the signing commitment, and optionally the nonce (for stateless signers)
Get Nonce for Commitment
getNonceForSelfCommitment(selfCommitment)
Retrieves the nonce associated with a previously generated commitment. In DefaultSparkSigner, this looks up the internally stored nonce. In UnsafeStatelessSparkSigner, this returns the nonce from the commitment object.
const commitment = await signer . getRandomSigningCommitment ();
const nonce = signer . getNonceForSelfCommitment ( commitment );
selfCommitment
SigningCommitmentWithOptionalNonce
required
The commitment returned from getRandomSigningCommitment()
The signing nonce, or undefined if not found
FROST Signing
signFrost(params)
Performs FROST signing operation. This produces the user’s signature share that will be combined with Signing Operator shares.
import { KeyDerivationType } from "@buildonspark/spark-sdk" ;
const commitment = await signer . getRandomSigningCommitment ();
const params = {
message: sighash , // Transaction sighash to sign
keyDerivation: { type: KeyDerivationType . LEAF , path: leafId },
publicKey: leafPublicKey ,
verifyingKey: leaf . verifyingPublicKey ,
selfCommitment: commitment ,
statechainCommitments: soCommitments , // From Signing Operators
adaptorPubKey: undefined // Optional adaptor for atomic swaps
};
const signatureShare = await signer . signFrost ( params );
FROST signing parameters:
message: The message (sighash) to sign
keyDerivation: How to derive the signing key
publicKey: The user’s public key for this leaf
verifyingKey: The aggregated public key (user + SOs)
selfCommitment: User’s signing commitment
statechainCommitments: Signing Operators’ commitments
adaptorPubKey: Optional adaptor public key
signatureShare
Promise<Uint8Array>
required
FROST signature share
Aggregate FROST Signatures
aggregateFrost(params)
Aggregates FROST signature shares (user’s + Signing Operators’) into a final Schnorr signature.
const params = {
message: sighash ,
publicKey: leafPublicKey ,
verifyingKey: leaf . verifyingPublicKey ,
selfCommitment: commitment ,
selfSignature: userSignatureShare ,
statechainCommitments: soCommitments ,
statechainSignatures: soSignatures ,
statechainPublicKeys: soPublicKeys
};
const finalSignature = await signer . aggregateFrost ( params );
params
AggregateFrostParams
required
FROST aggregation parameters including all signature shares and public keys
finalSignature
Promise<Uint8Array>
required
Final aggregated Schnorr signature (64 bytes)
Secret Sharing
These methods implement Shamir’s Secret Sharing with verifiable proofs, used internally for key splitting operations.
Split Secret with Proofs
splitSecretWithProofs(params)
Splits a secret into shares using Shamir’s Secret Sharing with verifiable proofs.
const params = {
secret: privateKey ,
curveOrder: secp256k1 . CURVE . n ,
threshold: 3 ,
numShares: 5
};
const shares = await signer . splitSecretWithProofs ( params );
params
SplitSecretWithProofsParams
required
secret: The secret to split (as Uint8Array)
curveOrder: The curve order (bigint)
threshold: Minimum shares needed to reconstruct
numShares: Total number of shares to generate
shares
Promise<VerifiableSecretShare[]>
required
Array of verifiable secret shares
Subtract and Split with Proofs
subtractAndSplitSecretWithProofsGivenDerivations(params)
Subtracts two derived private keys and splits the result into verifiable shares. Used in transfer flows.
import { KeyDerivationType } from "@buildonspark/spark-sdk" ;
const shares = await signer . subtractAndSplitSecretWithProofsGivenDerivations ({
first: { type: KeyDerivationType . LEAF , path: "old-leaf" },
second: { type: KeyDerivationType . LEAF , path: "new-leaf" },
curveOrder: secp256k1 . CURVE . n ,
threshold: 3 ,
numShares: 5
});
Subtract, Split, and Encrypt
subtractSplitAndEncrypt(params)
Subtracts keys, splits into shares, and encrypts the second key for the receiver. Used in transfer operations.
const result = await signer . subtractSplitAndEncrypt ({
first: { type: KeyDerivationType . LEAF , path: oldLeafId },
second: { type: KeyDerivationType . LEAF , path: newLeafId },
curveOrder: secp256k1 . CURVE . n ,
threshold: 3 ,
numShares: 5 ,
receiverPublicKey: receiverIdentityKey
});
console . log ( result . shares ); // Verifiable secret shares
console . log ( result . secretCipher ); // Encrypted key for receiver
Encryption
Decrypt ECIES
decryptEcies(ciphertext)
Decrypts ECIES-encrypted data using the identity key. Returns the public key corresponding to the decrypted private key.
const ciphertext = encryptedKeyFromSender ;
const publicKey = await signer . decryptEcies ( ciphertext );
ECIES-encrypted private key
publicKey
Promise<Uint8Array>
required
Public key corresponding to the decrypted private key
HTLC Operations
Generate HTLC HMAC
htlcHMAC(transferID)
Generates an HMAC for HTLC (Hash Time-Locked Contract) operations using the HTLC preimage key.
const hmac = await signer . htlcHMAC ( transferId );
The transfer ID to generate HMAC for
hmac
Promise<Uint8Array>
required
HMAC output (32 bytes)
Complete Example
import {
SparkWallet ,
DefaultSparkSigner ,
KeyDerivationType
} from "@buildonspark/spark-sdk" ;
async function demonstrateSparkSigner () {
// 1. Create and initialize signer
const signer = new DefaultSparkSigner ();
const mnemonic = await signer . generateMnemonic ();
console . log ( "Generated mnemonic:" , mnemonic );
const seed = await signer . mnemonicToSeed ( mnemonic );
const identityKeyHex = await signer . createSparkWalletFromSeed ( seed , 0 );
console . log ( "Identity key:" , identityKeyHex );
// 2. Get keys
const identityKey = await signer . getIdentityPublicKey ();
const depositKey = await signer . getDepositSigningKey ();
const staticDepositKey = await signer . getStaticDepositSigningKey ( 0 );
console . log ( "Keys initialized" );
// 3. Sign and validate a message
const message = new TextEncoder (). encode ( "Hello, Spark!" );
const signature = await signer . signMessageWithIdentityKey ( message );
const isValid = await signer . validateMessageWithIdentityKey ( message , signature );
console . log ( "Signature valid:" , isValid );
// 4. Schnorr signature
const schnorrSig = await signer . signSchnorrWithIdentityKey ( message );
console . log ( "Schnorr signature length:" , schnorrSig . length );
// 5. Get public key from derivation
const leafPubKey = await signer . getPublicKeyFromDerivation ({
type: KeyDerivationType . LEAF ,
path: "my-leaf-id"
});
console . log ( "Leaf public key:" , leafPubKey );
// 6. Use with SparkWallet
const { wallet } = await SparkWallet . initialize ({
mnemonicOrSeed: mnemonic ,
accountNumber: 0 ,
options: { network: "REGTEST" }
});
const address = await wallet . getSparkAddress ();
console . log ( "Spark address:" , address );
}
Integration Patterns
Remote Signer (Enclave Pattern)
For wallet providers that need to keep keys in a secure enclave:
class RemoteSigner extends DefaultSparkSigner {
private apiClient : EnclaveAPIClient ;
private userId : string ;
constructor ( apiClient : EnclaveAPIClient , userId : string ) {
super ();
this . apiClient = apiClient ;
this . userId = userId ;
}
async signFrost ( params : SignFrostParams ) : Promise < Uint8Array > {
// Forward to enclave
return this . apiClient . signFrost ( this . userId , params );
}
async aggregateFrost ( params : AggregateFrostParams ) : Promise < Uint8Array > {
return this . apiClient . aggregateFrost ( this . userId , params );
}
async createSparkWalletFromSeed (
seed : Uint8Array | string ,
accountNumber ?: number
) : Promise < string > {
// Keys are managed in the enclave
return this . apiClient . initializeWallet ( this . userId , seed , accountNumber );
}
}
Multi-User Wallet Pattern
For services managing wallets for multiple users:
class MultiUserSigner extends UnsafeStatelessSparkSigner {
private keyStore : KeyStore ;
async signFrost ( params : SignFrostParams ) : Promise < Uint8Array > {
// Look up user's key material from secure storage
const userKeys = await this . keyStore . getKeys ( params . publicKey );
// Perform signing with user's keys
return super . signFrost ({
... params ,
// Additional context if needed
});
}
}
For multi-user wallets, consider the trust model carefully. See the Alby architecture blog post for a trust-minimized approach using NWC.