UNPKG

@btc-vision/transaction

Version:

OPNet transaction library allows you to create and sign transactions for the OPNet network.

841 lines (619 loc) 31.6 kB
# MessageSigner The `MessageSigner` is a singleton instance exported from `@btc-vision/transaction` that provides a unified API for cryptographic message signing and verification. It supports Schnorr signatures, Taproot-tweaked Schnorr signatures, and quantum-resistant ML-DSA (FIPS 204) signatures. Its most important feature is a set of **Auto methods** that transparently detect whether the code is running in a browser (with the OP_WALLET extension) or on a backend server, and delegate signing accordingly. ## Table of Contents - [Architecture Overview](#architecture-overview) - [Interfaces](#interfaces) - [SignedMessage](#signedmessage) - [MLDSASignedMessage](#mldsasignedmessage) - [Environment Detection](#environment-detection) - [isOPWalletAvailable](#isopwalletavailable) - [Auto Methods (Recommended)](#auto-methods-recommended) - [signMessageAuto](#signmessageauto) - [tweakAndSignMessageAuto](#tweakandsignmessageauto) - [signMLDSAMessageAuto](#signmldsamessageauto) - [Non-Auto Methods (Backend Only)](#non-auto-methods-backend-only) - [signMessage](#signmessage) - [tweakAndSignMessage](#tweakandsignmessage) - [signMLDSAMessage](#signmldsamessage) - [Browser Wallet Methods](#browser-wallet-methods) - [trySignSchnorrWithOPWallet](#trysignschnorrwithopwallet) - [trySignMLDSAWithOPWallet](#trysignmldsawithopwallet) - [verifyMLDSAWithOPWallet](#verifymldsawithopwallet) - [getMLDSAPublicKeyFromOPWallet](#getmldsapublickeyfromopwallet) - [Verification Methods](#verification-methods) - [verifySignature](#verifysignature) - [tweakAndVerifySignature](#tweakandverifysignature) - [verifyMLDSASignature](#verifymldsasignature) - [Utility Methods](#utility-methods) - [sha256](#sha256) - [Auto vs Non-Auto: Critical Guidance](#auto-vs-non-auto-critical-guidance) - [Code Examples](#code-examples) - [Backend Schnorr Signing](#backend-schnorr-signing) - [Backend Tweaked Signing](#backend-tweaked-signing) - [Backend ML-DSA Signing](#backend-ml-dsa-signing) - [Browser Auto Signing](#browser-auto-signing) - [Universal Code with Auto Methods](#universal-code-with-auto-methods) - [Full Verification Workflow](#full-verification-workflow) - [Best Practices](#best-practices) - [Navigation](#navigation) ## Architecture Overview The MessageSigner auto-detection flow determines which signing backend to use at runtime: ```mermaid flowchart TD Call["signMessageAuto(message, keypair?)"] KP{"keypair\nprovided?"} Local["Local Signing\n(Backend)"] CheckWallet{"OP_WALLET\navailable?"} WalletSign["OP_WALLET Signing\n(Browser Extension)"] Error["Throw Error\n'No keypair provided\nand OP_WALLET is\nnot available.'"] Result["SignedMessage"] Call --> KP KP -->|"Yes"| Local KP -->|"No / undefined"| CheckWallet CheckWallet -->|"Yes"| WalletSign CheckWallet -->|"No"| Error Local --> Result WalletSign --> Result ``` The same flow applies to `tweakAndSignMessageAuto` and `signMLDSAMessageAuto`. The private `getOPWallet()` method checks for `window.opnet` and validates it with the `isOPWallet` type guard before delegating to the browser extension. ## Interfaces ### SignedMessage Returned by all Schnorr signing methods (both standard and tweaked). ```typescript interface SignedMessage { readonly signature: Uint8Array; // 64-byte Schnorr signature readonly message: Uint8Array; // SHA-256 hash of the original message } ``` ### MLDSASignedMessage Returned by all ML-DSA signing methods. Includes additional metadata required for quantum-resistant verification. ```typescript interface MLDSASignedMessage { readonly signature: Uint8Array; // ML-DSA signature (2420-4627 bytes depending on level) readonly message: Uint8Array; // SHA-256 hash of the original message readonly publicKey: Uint8Array; // ML-DSA public key used for signing readonly securityLevel: MLDSASecurityLevel; // LEVEL2, LEVEL3, or LEVEL5 } ``` ## Environment Detection ### isOPWalletAvailable Checks whether the OP_WALLET browser extension is present and valid. ```typescript isOPWalletAvailable(): boolean ``` **Returns:** `true` if `window.opnet` exists and satisfies the `OPWallet` interface, `false` otherwise. Always returns `false` in Node.js / non-browser environments (where `window` is undefined). **Example:** ```typescript import { MessageSigner } from '@btc-vision/transaction'; if (MessageSigner.isOPWalletAvailable()) { console.log('OP_WALLET detected - browser signing available'); } else { console.log('No OP_WALLET - provide a keypair for signing'); } ``` ## Auto Methods (Recommended) The Auto methods are the **primary recommended API** for signing messages. They unify browser and backend signing behind a single call, making your code portable across environments. **Key rule:** When `keypair` is `undefined` or omitted, the method attempts browser signing via OP_WALLET. When `keypair` is provided, it signs locally using the keypair directly. ### signMessageAuto Signs a message with a Schnorr signature, automatically selecting browser or backend signing. ```typescript async signMessageAuto( message: Uint8Array | string, keypair?: UniversalSigner ): Promise<SignedMessage> ``` **Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `message` | `Uint8Array \| string` | Yes | The message to sign. Strings are UTF-8 encoded, then SHA-256 hashed before signing. | | `keypair` | `UniversalSigner` | No | The signing keypair. Omit for browser (OP_WALLET) signing; provide for backend signing. | **Returns:** `Promise<SignedMessage>` containing the 64-byte Schnorr signature and the SHA-256 hashed message. **Throws:** - `Error('No keypair provided and OP_WALLET is not available.')` if no keypair is given and OP_WALLET is not detected. ### tweakAndSignMessageAuto Signs a message with a Taproot-tweaked Schnorr signature, automatically selecting browser or backend signing. ```typescript async tweakAndSignMessageAuto( message: Uint8Array | string, keypair?: UniversalSigner, network?: Network ): Promise<SignedMessage> ``` **Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `message` | `Uint8Array \| string` | Yes | The message to sign. Strings are UTF-8 encoded, then SHA-256 hashed before signing. | | `keypair` | `UniversalSigner` | No | The signing keypair. Omit for browser signing; provide for backend signing. | | `network` | `Network` | Conditionally | **Required when `keypair` is provided** (backend signing). The Bitcoin network configuration (`networks.bitcoin`, `networks.testnet`, or `networks.regtest`). Ignored when using OP_WALLET. | **Returns:** `Promise<SignedMessage>` containing the 64-byte tweaked Schnorr signature and the SHA-256 hashed message. **Throws:** - `Error('No keypair provided and OP_WALLET is not available.')` if no keypair is given and OP_WALLET is not detected. - `Error('Network is required when signing with a local keypair.')` if a keypair is provided but `network` is omitted. **Note:** When signing via OP_WALLET (no keypair), the wallet handles tweaking internally through `wallet.web3.signSchnorr()`. The `network` parameter is not needed in this case. ### signMLDSAMessageAuto Signs a message with a quantum-resistant ML-DSA signature, automatically selecting browser or backend signing. ```typescript async signMLDSAMessageAuto( message: Uint8Array | string, mldsaKeypair?: QuantumBIP32Interface ): Promise<MLDSASignedMessage> ``` **Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `message` | `Uint8Array \| string` | Yes | The message to sign. Strings are UTF-8 encoded, then SHA-256 hashed before signing. | | `mldsaKeypair` | `QuantumBIP32Interface` | No | The ML-DSA keypair. Omit for browser (OP_WALLET) signing; provide for backend signing. | **Returns:** `Promise<MLDSASignedMessage>` containing the ML-DSA signature, hashed message, public key, and security level. **Throws:** - `Error('No ML-DSA keypair provided and OP_WALLET is not available.')` if no keypair is given and OP_WALLET is not detected. ## Non-Auto Methods (Backend Only) These methods sign directly with a provided keypair. They do **not** check for OP_WALLET and will throw errors if a valid keypair is not provided. Use these only when you are certain you are running in a backend environment with access to private keys. ### signMessage Signs a message with a standard Schnorr signature using a local keypair. ```typescript signMessage( keypair: UniversalSigner, message: Uint8Array | string ): SignedMessage ``` **Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `keypair` | `UniversalSigner` | Yes | The signing keypair. Must contain a `privateKey`. | | `message` | `Uint8Array \| string` | Yes | The message to sign. Strings are UTF-8 encoded, then SHA-256 hashed. | **Returns:** `SignedMessage` (synchronous, not a Promise). **Throws:** - `Error('Private key not found in keypair.')` if `keypair.privateKey` is falsy. - `Error('backend.signSchnorr is not available.')` if the ECC backend is not initialized. ### tweakAndSignMessage Signs a message with a Taproot-tweaked Schnorr signature using a local keypair. ```typescript tweakAndSignMessage( keypair: UniversalSigner, message: Uint8Array | string, network: Network ): SignedMessage ``` **Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `keypair` | `UniversalSigner` | Yes | The signing keypair. Must contain a `privateKey`. | | `message` | `Uint8Array \| string` | Yes | The message to sign. | | `network` | `Network` | Yes | The Bitcoin network. Required for computing the Taproot tweak. | **Returns:** `SignedMessage` (synchronous). **Internal behavior:** Applies `TweakedSigner.tweakSigner(keypair, { network })` to derive a tweaked keypair, then delegates to `signMessage`. ### signMLDSAMessage Signs a message with a quantum-resistant ML-DSA signature using a local keypair. ```typescript signMLDSAMessage( mldsaKeypair: QuantumBIP32Interface, message: Uint8Array | string ): MLDSASignedMessage ``` **Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `mldsaKeypair` | `QuantumBIP32Interface` | Yes | The ML-DSA keypair from BIP32 quantum wallet derivation. Must contain a `privateKey`. | | `message` | `Uint8Array \| string` | Yes | The message to sign. | **Returns:** `MLDSASignedMessage` (synchronous). **Throws:** - `Error('ML-DSA private key not found in keypair.')` if `mldsaKeypair.privateKey` is falsy. ## Browser Wallet Methods These methods interact directly with the OP_WALLET browser extension. They return `null` when OP_WALLET is not available (instead of throwing), making them safe to call speculatively. The Auto methods use these internally. ### trySignSchnorrWithOPWallet Attempts to sign a message via the OP_WALLET browser extension using Schnorr. ```typescript async trySignSchnorrWithOPWallet( message: Uint8Array | string ): Promise<SignedMessage | null> ``` **Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `message` | `Uint8Array \| string` | Yes | The message to sign. | **Returns:** `Promise<SignedMessage | null>`. Returns `null` if OP_WALLET is not available. Otherwise returns the signed message from the wallet extension. **Internal behavior:** Hashes the message with SHA-256, converts to hex, and calls `wallet.web3.signSchnorr(messageHex)`. ### trySignMLDSAWithOPWallet Attempts to sign a message via the OP_WALLET browser extension using ML-DSA. ```typescript async trySignMLDSAWithOPWallet( message: Uint8Array | string ): Promise<MLDSASignedMessage | null> ``` **Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `message` | `Uint8Array \| string` | Yes | The message to sign. | **Returns:** `Promise<MLDSASignedMessage | null>`. Returns `null` if OP_WALLET is not available. Otherwise returns the ML-DSA signed message including the public key and security level from the wallet. **Internal behavior:** Hashes the message with SHA-256, converts to hex, and calls `wallet.web3.signMLDSAMessage(messageHex)`. ### verifyMLDSAWithOPWallet Verifies an ML-DSA signature via the OP_WALLET browser extension. ```typescript async verifyMLDSAWithOPWallet( message: Uint8Array | string, signature: MLDSASignedMessage ): Promise<boolean | null> ``` **Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `message` | `Uint8Array \| string` | Yes | The original message that was signed. | | `signature` | `MLDSASignedMessage` | Yes | The full ML-DSA signed message object to verify. | **Returns:** `Promise<boolean | null>`. Returns `null` if OP_WALLET is not available. Returns `true` or `false` for the verification result from the wallet. ### getMLDSAPublicKeyFromOPWallet Retrieves the user's ML-DSA public key from the OP_WALLET browser extension. ```typescript async getMLDSAPublicKeyFromOPWallet(): Promise<Uint8Array | null> ``` **Returns:** `Promise<Uint8Array | null>`. Returns `null` if OP_WALLET is not available. Otherwise returns the ML-DSA public key as a `Uint8Array`. ## Verification Methods All verification methods are synchronous and work in both browser and backend environments. They do not require private keys. ### verifySignature Verifies a standard Schnorr signature against a public key and message. ```typescript verifySignature( publicKey: Uint8Array, message: Uint8Array | string, signature: Uint8Array ): boolean ``` **Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `publicKey` | `Uint8Array` | Yes | The signer's public key (33-byte compressed or 32-byte x-only). Internally converted to x-only via `toXOnly`. | | `message` | `Uint8Array \| string` | Yes | The original message (not the hash). The method hashes it with SHA-256 internally. | | `signature` | `Uint8Array` | Yes | The 64-byte Schnorr signature to verify. | **Returns:** `boolean` -- `true` if the signature is valid, `false` otherwise. **Throws:** - `Error('Invalid signature length.')` if `signature.length !== 64`. - `Error('backend.verifySchnorr is not available.')` if the ECC backend is not initialized. ### tweakAndVerifySignature Verifies a Taproot-tweaked Schnorr signature. Internally tweaks the provided public key with `EcKeyPair.tweakPublicKey()` then delegates to `verifySignature`. ```typescript tweakAndVerifySignature( publicKey: Uint8Array, message: Uint8Array | string, signature: Uint8Array ): boolean ``` **Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `publicKey` | `Uint8Array` | Yes | The signer's **untweaked** public key. The method applies the Taproot tweak internally. | | `message` | `Uint8Array \| string` | Yes | The original message (not the hash). | | `signature` | `Uint8Array` | Yes | The 64-byte tweaked Schnorr signature. | **Returns:** `boolean` ### verifyMLDSASignature Verifies a quantum-resistant ML-DSA signature. ```typescript verifyMLDSASignature( mldsaKeypair: QuantumBIP32Interface, message: Uint8Array | string, signature: Uint8Array ): boolean ``` **Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `mldsaKeypair` | `QuantumBIP32Interface` | Yes | An ML-DSA keypair containing the public key. Can be a full keypair or a public-key-only keypair created via `QuantumBIP32Factory.fromPublicKey()`. | | `message` | `Uint8Array \| string` | Yes | The original message (not the hash). | | `signature` | `Uint8Array` | Yes | The ML-DSA signature bytes. | **Returns:** `boolean` **Note:** Unlike Schnorr verification which takes a raw public key, ML-DSA verification requires a keypair object because it embeds security level information and the ML-DSA verification algorithm structure. If you only have a raw public key, reconstruct a keypair with `QuantumBIP32Factory.fromPublicKey()`. ## Utility Methods ### sha256 Computes the SHA-256 hash of a message. ```typescript sha256(message: Uint8Array): Uint8Array ``` **Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `message` | `Uint8Array` | Yes | The data to hash. | **Returns:** `Uint8Array` -- 32-byte SHA-256 digest. **Note:** All signing methods call `sha256` internally before signing. You do not need to pre-hash messages unless you have a specific reason to do so (such as signing the same hash with multiple algorithms). ## Auto vs Non-Auto: Critical Guidance This section explains the most important architectural decision in the MessageSigner API. Choosing incorrectly between Auto and Non-Auto methods will cause runtime crashes. ### The Problem OPNet applications can run in two very different environments: 1. **Browser** -- The user's private keys are held by the OP_WALLET browser extension. Your JavaScript code does **not** have access to private keys. Signing is delegated to `window.opnet.web3.signSchnorr()` and related wallet methods. 2. **Backend (Node.js)** -- Your server has direct access to private keys via keypair objects (`UniversalSigner`, `QuantumBIP32Interface`). Signing is done locally using the ECC/ML-DSA backends. ### The Solution: Auto Methods The Auto methods (`signMessageAuto`, `tweakAndSignMessageAuto`, `signMLDSAMessageAuto`) solve this by using a simple convention: ```mermaid flowchart LR subgraph Browser["Browser Environment"] B1["signMessageAuto(msg)"] B2["keypair = undefined"] B3["OP_WALLET signs"] B1 --> B2 --> B3 end subgraph Backend["Backend Environment"] S1["signMessageAuto(msg, keypair)"] S2["keypair = provided"] S3["Local keypair signs"] S1 --> S2 --> S3 end ``` | Scenario | `keypair` parameter | What happens | |----------|---------------------|--------------| | Browser with OP_WALLET | `undefined` (omitted) | OP_WALLET handles signing via browser extension | | Backend with keypair | Provided (`UniversalSigner`) | Local signing with the keypair's private key | | Browser without OP_WALLET | `undefined` (omitted) | **Throws error** -- no signing mechanism available | | Backend without keypair | `undefined` (omitted) | Checks OP_WALLET (fails in Node.js), then **throws error** | ### When to Use Auto Methods **Always use Auto methods when writing library code or shared modules** that may run in either environment. They are the safe, portable choice. ```typescript // This function works in BOTH browser and backend async function signTransaction(message: string, keypair?: UniversalSigner) { return MessageSigner.signMessageAuto(message, keypair); } ``` ### When to Use Non-Auto Methods Use Non-Auto methods **only** when you are 100% certain you are in a backend environment and you want synchronous behavior. The Non-Auto methods (`signMessage`, `tweakAndSignMessage`, `signMLDSAMessage`) are synchronous and return `SignedMessage` directly (not a Promise). ```typescript // Backend-only code -- this WILL crash in a browser without a keypair const signed = MessageSigner.signMessage(keypair, message); ``` ### Common Mistakes **Mistake 1: Using Non-Auto methods in browser code** ```typescript // WRONG -- will crash in browser because there is no keypair const signed = MessageSigner.signMessage(undefined as any, message); // CORRECT -- use Auto method, which falls back to OP_WALLET const signed = await MessageSigner.signMessageAuto(message); ``` **Mistake 2: Forgetting the network parameter for tweaked backend signing** ```typescript // WRONG -- network is required for local tweaked signing const signed = await MessageSigner.tweakAndSignMessageAuto(message, keypair); // CORRECT -- provide network when keypair is present const signed = await MessageSigner.tweakAndSignMessageAuto(message, keypair, network); ``` **Mistake 3: Not awaiting Auto methods** ```typescript // WRONG -- Auto methods return Promises const signed = MessageSigner.signMessageAuto(message, keypair); console.log(signed.signature); // undefined! signed is a Promise // CORRECT const signed = await MessageSigner.signMessageAuto(message, keypair); console.log(signed.signature); // Uint8Array ``` ## Code Examples ### Backend Schnorr Signing ```typescript import { MessageSigner, EcKeyPair } from '@btc-vision/transaction'; import { networks, toHex } from '@btc-vision/bitcoin'; const network = networks.bitcoin; const keypair = EcKeyPair.fromWIF('your-private-key-wif', network); // Sign a message (synchronous, backend only) const message = 'Authenticate this action'; const signed = MessageSigner.signMessage(keypair, message); console.log('Signature:', toHex(signed.signature)); // 64-byte hex console.log('Message hash:', toHex(signed.message)); // 32-byte SHA-256 hex // Verify the signature const isValid = MessageSigner.verifySignature( keypair.publicKey, message, // Pass the original message, not the hash signed.signature, ); console.log('Valid:', isValid); // true ``` ### Backend Tweaked Signing ```typescript import { MessageSigner, EcKeyPair } from '@btc-vision/transaction'; import { networks, toHex } from '@btc-vision/bitcoin'; const network = networks.bitcoin; const keypair = EcKeyPair.fromWIF('your-private-key-wif', network); // Sign with Taproot tweak (synchronous, backend only) const message = 'Taproot-compatible signature'; const signed = MessageSigner.tweakAndSignMessage(keypair, message, network); console.log('Tweaked signature:', toHex(signed.signature)); // Verify with the UNTWEAKED public key // tweakAndVerifySignature applies the tweak internally const isValid = MessageSigner.tweakAndVerifySignature( keypair.publicKey, // Untweaked public key message, signed.signature, ); console.log('Valid:', isValid); // true ``` ### Backend ML-DSA Signing ```typescript import { MessageSigner, Mnemonic, QuantumBIP32Factory, MLDSASecurityLevel, } from '@btc-vision/transaction'; import { networks, toHex } from '@btc-vision/bitcoin'; const network = networks.bitcoin; const securityLevel = MLDSASecurityLevel.LEVEL2; // Generate a quantum wallet const mnemonic = Mnemonic.generate(undefined, '', network, securityLevel); const wallet = mnemonic.derive(0); // Sign with ML-DSA (synchronous, backend only) const message = 'Quantum-resistant authentication'; const signed = MessageSigner.signMLDSAMessage(wallet.mldsaKeypair, message); console.log('Signature size:', signed.signature.length, 'bytes'); // 2420 for LEVEL2 console.log('Public key size:', signed.publicKey.length, 'bytes'); // 1312 for LEVEL2 console.log('Security level:', signed.securityLevel); // Verify with the original keypair const isValid = MessageSigner.verifyMLDSASignature( wallet.mldsaKeypair, message, signed.signature, ); console.log('Valid:', isValid); // true // Verify with a public-key-only keypair (e.g., on a different machine) const publicKeyOnly = QuantumBIP32Factory.fromPublicKey( signed.publicKey, wallet.chainCode, network, securityLevel, ); const isValidRemote = MessageSigner.verifyMLDSASignature( publicKeyOnly, message, signed.signature, ); console.log('Remote verification:', isValidRemote); // true ``` ### Browser Auto Signing ```typescript import { MessageSigner } from '@btc-vision/transaction'; // In a browser with OP_WALLET installed: // No keypair needed -- OP_WALLET handles everything async function browserSign() { // Check wallet availability (optional, Auto methods handle this) if (!MessageSigner.isOPWalletAvailable()) { alert('Please install the OP_WALLET extension'); return; } // Schnorr signing via OP_WALLET const schnorrSigned = await MessageSigner.signMessageAuto( 'Sign this with your wallet', ); console.log('Schnorr signature:', schnorrSigned.signature); // Tweaked signing via OP_WALLET (no network needed) const tweakedSigned = await MessageSigner.tweakAndSignMessageAuto( 'Taproot-compatible browser signature', ); console.log('Tweaked signature:', tweakedSigned.signature); // ML-DSA signing via OP_WALLET const mldsaSigned = await MessageSigner.signMLDSAMessageAuto( 'Quantum-resistant browser signature', ); console.log('ML-DSA signature:', mldsaSigned.signature); console.log('ML-DSA public key:', mldsaSigned.publicKey); console.log('Security level:', mldsaSigned.securityLevel); } ``` ### Universal Code with Auto Methods This pattern writes code that works in **both** browser and backend environments without modification. ```typescript import { MessageSigner } from '@btc-vision/transaction'; import type { UniversalSigner } from '@btc-vision/ecpair'; import type { Network } from '@btc-vision/bitcoin'; /** * Signs an authentication challenge. * - In the browser: call with just the message (OP_WALLET signs). * - On the backend: call with the message and a keypair. */ async function signAuthChallenge( challenge: string, keypair?: UniversalSigner, ): Promise<Uint8Array> { const signed = await MessageSigner.signMessageAuto(challenge, keypair); return signed.signature; } /** * Signs with Taproot tweak. * - In the browser: call without keypair/network. * - On the backend: call with keypair and network. */ async function signTweakedChallenge( challenge: string, keypair?: UniversalSigner, network?: Network, ): Promise<Uint8Array> { const signed = await MessageSigner.tweakAndSignMessageAuto( challenge, keypair, network, ); return signed.signature; } // Browser usage: // const sig = await signAuthChallenge('challenge-string'); // Backend usage: // const sig = await signAuthChallenge('challenge-string', myKeypair); ``` ### Full Verification Workflow ```typescript import { MessageSigner, EcKeyPair, Mnemonic, QuantumBIP32Factory, MLDSASecurityLevel, } from '@btc-vision/transaction'; import { networks, toHex, fromHex } from '@btc-vision/bitcoin'; const network = networks.regtest; // --- Schnorr --- const keypair = EcKeyPair.generateRandomKeyPair(network); const message = 'Verify all the things'; const schnorrSigned = MessageSigner.signMessage(keypair, message); const schnorrValid = MessageSigner.verifySignature( keypair.publicKey, message, schnorrSigned.signature, ); console.log('Schnorr valid:', schnorrValid); // true // Tampered message fails const schnorrInvalid = MessageSigner.verifySignature( keypair.publicKey, 'Wrong message', schnorrSigned.signature, ); console.log('Schnorr tampered:', schnorrInvalid); // false // --- Tweaked Schnorr --- const tweakedSigned = MessageSigner.tweakAndSignMessage(keypair, message, network); const tweakedValid = MessageSigner.tweakAndVerifySignature( keypair.publicKey, // Pass the UNTWEAKED key message, tweakedSigned.signature, ); console.log('Tweaked valid:', tweakedValid); // true // --- ML-DSA --- const securityLevel = MLDSASecurityLevel.LEVEL2; const mnemonic = Mnemonic.generate(undefined, '', network, securityLevel); const wallet = mnemonic.derive(0); const mldsaSigned = MessageSigner.signMLDSAMessage(wallet.mldsaKeypair, message); // Verify with public-key-only reconstruction (simulating remote verification) const remoteKeypair = QuantumBIP32Factory.fromPublicKey( mldsaSigned.publicKey, wallet.chainCode, network, securityLevel, ); const mldsaValid = MessageSigner.verifyMLDSASignature( remoteKeypair, message, mldsaSigned.signature, ); console.log('ML-DSA valid:', mldsaValid); // true // --- Browser ML-DSA verification via OP_WALLET --- // (only works in browser with OP_WALLET) const walletResult = await MessageSigner.verifyMLDSAWithOPWallet(message, mldsaSigned); if (walletResult !== null) { console.log('OP_WALLET ML-DSA valid:', walletResult); } else { console.log('OP_WALLET not available, skipping wallet verification'); } ``` ## Best Practices 1. **Default to Auto methods.** Unless you have a specific reason to use Non-Auto methods (synchronous signing in a guaranteed backend context), always prefer `signMessageAuto`, `tweakAndSignMessageAuto`, and `signMLDSAMessageAuto`. They handle environment detection for you and future-proof your code. 2. **Always verify signatures.** Never trust signed data without verification, especially data received over a network. Use `verifySignature`, `tweakAndVerifySignature`, or `verifyMLDSASignature` before acting on signed messages. 3. **Pass original messages to verification methods.** All verification methods hash the message internally with SHA-256. Pass the original message string or bytes, not a pre-computed hash. The only exception is if both signer and verifier explicitly agree on a pre-hashed protocol. 4. **Pass the untweaked public key to `tweakAndVerifySignature`.** The method applies the Taproot tweak internally. Passing an already-tweaked key will produce a double-tweaked key and verification will fail. 5. **Provide `network` for tweaked backend signing.** When calling `tweakAndSignMessageAuto` or `tweakAndSignMessage` with a local keypair, the `network` parameter is required for computing the correct Taproot tweak. Omitting it throws an error. 6. **Use `QuantumBIP32Factory.fromPublicKey()` for remote ML-DSA verification.** When you only have a public key (not the original keypair), reconstruct a public-key-only keypair before calling `verifyMLDSASignature`. This is required because the verification algorithm needs security level metadata embedded in the keypair object. 7. **Structure signed messages with context.** Include action type, timestamps, and nonces in your signed messages to prevent replay attacks: ```typescript const message = JSON.stringify({ action: 'transfer', amount: '50000', timestamp: Date.now(), nonce: toHex(crypto.getRandomValues(new Uint8Array(16))), }); const signed = await MessageSigner.signMessageAuto(message, keypair); ``` 8. **Never expose private keys.** The OP_WALLET architecture exists specifically to keep private keys inside the browser extension. In backend code, load keys from secure storage (environment variables, HSM, encrypted vaults) and avoid logging them. 9. **Handle OP_WALLET absence gracefully.** In browser code, check `isOPWalletAvailable()` before signing to provide a user-friendly error message instead of an unhandled exception. 10. **Match security levels for ML-DSA.** When verifying ML-DSA signatures, the security level used for `QuantumBIP32Factory.fromPublicKey()` must match the level used during signing. Mismatched levels cause verification failure. ## Navigation - [Transaction Building Guide](../transaction-building/transaction-factory.md) - [Offline Transaction Signing](../offline/offline-transaction-signing.md) - [Quantum Support: Message Signing](../quantum-support/04-message-signing.md) - [Documentation Index](../README.md)