TypeScript implementation of secp256k1 key pairs and three signing schemes used by did:btcr2.
Part of the did-btcr2-js monorepo.
This package wraps the secp256k1 primitives from @noble/curves into the key types and signing abstraction used by every higher-level package (@did-btcr2/cryptosuite, @did-btcr2/key-manager, @did-btcr2/method).
SchnorrKeyPair owns a Secp256k1SecretKey plus a CompressedSecp256k1PublicKey. Generate fresh, import from raw bytes, hex, or JSON.CompressedSecp256k1PublicKey exposes the 33-byte compressed form, the 32-byte x-only form, and the multibase-encoded form (zQ3s...) used in DID verification methods.Secp256k1SecretKey holds the 32-byte raw secret with validation against the curve order.LocalSigner is the in-process signer that backs the Signer interface. It supports all three signing schemes the DID method needs.| Scheme | Wire format | Used for |
|---|---|---|
'ecdsa' |
DER-encoded, low-S, no prehash | P2PKH / P2WPKH / P2SH-P2WPKH input signatures (BIP-143 sighash) |
'bip340' |
64-byte raw Schnorr | Data Integrity proofs over DID updates (untweaked) |
'bip341' |
64-byte Schnorr over taproot-tweaked key | P2TR input signatures (BIP-341 key-path) |
The bip341 path applies taprootTweakPrivKey(secret, merkleRoot) before signing so the produced signature verifies against the tweaked output internal key Q = P + tG.
npm install @did-btcr2/keypair
Or with pnpm:
pnpm add @did-btcr2/keypair
Requires Node >= 22. Ships both ESM (dist/esm/) and CJS (dist/cjs/) with TypeScript declarations.
| Concern | Entry point |
|---|---|
| Generate a fresh keypair | SchnorrKeyPair.generate() |
| Import from bytes / hex / JSON | SchnorrKeyPair.fromSecret(), .fromJSON() |
| Watch-only pair (pubkey only) | new SchnorrKeyPair({ publicKey }) |
| Sign with explicit scheme | LocalSigner, signWithScheme(secret, data, scheme) |
| Signer interface | Signer, SigningScheme, SignOptions |
| Public key types | CompressedSecp256k1PublicKey, PublicKey, Point |
| Secret key type | Secp256k1SecretKey, SecretKey |
| Multibase prefix | BIP340_PUBLIC_KEY_MULTIBASE_PREFIX ([0xe7, 0x01]) |
import { LocalSigner, SchnorrKeyPair, signWithScheme } from '@did-btcr2/keypair';
// Generate a fresh keypair.
const kp = SchnorrKeyPair.generate();
// Compressed pubkey + x-only pubkey + multibase form.
const compressed = kp.publicKey.compressed; // 33 bytes
const xOnly = kp.publicKey.xOnly; // 32 bytes
const multibase = kp.publicKey.multibase.encoded; // 'zQ3s...'
// Build a Signer and sign with the appropriate scheme.
const signer = new LocalSigner(kp.secretKey.bytes);
const data = new Uint8Array(32);
const sigEcdsa = signer.sign(data, 'ecdsa'); // for P2PKH/P2WPKH/P2SH inputs
const sigBip340 = signer.sign(data, 'bip340'); // for DI proofs
const sigBip341 = signer.sign(data, 'bip341', { merkleRoot: new Uint8Array(0) });
@noble/curves; the BIP-341 taproot tweak uses @scure/btc-signer. No bitcoinjs-lib, no elliptic, no node:crypto.LocalSigner constructed without a secret throws on sign(); a watch-only key throws on secretKey access. Failures surface at the boundary they happen at.signWithScheme() is the one function that knows how to produce each scheme; both LocalSigner and the key manager delegate to it.# From packages/keypair/
pnpm build # Compile ESM + CJS + type declarations
pnpm build:tests # Compile tests to tests/compiled/
pnpm test # Run the test suite with coverage
pnpm lint # ESLint (zero warnings tolerated)
SchnorrKeyPair, LocalSigner, and signWithScheme.