UNPKG

@btc-vision/transaction

Version:

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

855 lines (650 loc) 25.4 kB
# Message Signing Guide ## Table of Contents - [ML-DSA Signing](#ml-dsa-signing) - [Schnorr Signing](#schnorr-signing) - [Input Formats](#input-formats) - [Signature Verification](#signature-verification) - [Tweaked Signatures](#tweaked-signatures) - [Best Practices](#best-practices) ## ML-DSA Signing ### Basic ML-DSA Signing Sign messages with quantum-resistant ML-DSA signatures: ```typescript import { Mnemonic, MessageSigner, MLDSASecurityLevel } from '@btc-vision/transaction'; import { networks, toHex } from '@btc-vision/bitcoin'; // Generate wallet const mnemonic = Mnemonic.generate(undefined, '', networks.bitcoin, MLDSASecurityLevel.LEVEL2); const wallet = mnemonic.derive(0); // Sign a message const message = 'Hello, Quantum World!'; const signed = MessageSigner.signMLDSAMessage(wallet.mldsaKeypair, message); console.log('Message:', signed.message); console.log('Signature:', toHex(signed.signature)); console.log('Public Key:', toHex(signed.publicKey)); console.log('Security Level:', signed.securityLevel); ``` ### ML-DSA Signature Sizes Different security levels produce different signature sizes: ```typescript // LEVEL2 (ML-DSA-44) const level2Mnemonic = Mnemonic.generate(undefined, '', networks.bitcoin, MLDSASecurityLevel.LEVEL2); const level2Wallet = level2Mnemonic.derive(0); const level2Sig = MessageSigner.signMLDSAMessage(level2Wallet.mldsaKeypair, 'test'); console.log('LEVEL2 Signature Size:', level2Sig.signature.length); // 2420 bytes // LEVEL3 (ML-DSA-65) const level3Mnemonic = Mnemonic.generate(undefined, '', networks.bitcoin, MLDSASecurityLevel.LEVEL3); const level3Wallet = level3Mnemonic.derive(0); const level3Sig = MessageSigner.signMLDSAMessage(level3Wallet.mldsaKeypair, 'test'); console.log('LEVEL3 Signature Size:', level3Sig.signature.length); // 3309 bytes // LEVEL5 (ML-DSA-87) const level5Mnemonic = Mnemonic.generate(undefined, '', networks.bitcoin, MLDSASecurityLevel.LEVEL5); const level5Wallet = level5Mnemonic.derive(0); const level5Sig = MessageSigner.signMLDSAMessage(level5Wallet.mldsaKeypair, 'test'); console.log('LEVEL5 Signature Size:', level5Sig.signature.length); // 4627 bytes ``` ### Verifying ML-DSA Signatures When verifying signatures, you need to create a public-key-only keypair using `QuantumBIP32Factory.fromPublicKey()`: ```typescript import { MessageSigner, QuantumBIP32Factory } from '@btc-vision/transaction'; // Sign message const message = 'Verify this quantum signature'; const signed = MessageSigner.signMLDSAMessage(wallet.mldsaKeypair, message); // Create public-key-only keypair for verification const publicKeyPair = QuantumBIP32Factory.fromPublicKey( signed.publicKey, // ML-DSA public key from signature wallet.chainCode, // Chain code from wallet network, // Network (mainnet/testnet/regtest) securityLevel // ML-DSA security level (LEVEL2/LEVEL3/LEVEL5) ); // Verify signature const isValid = MessageSigner.verifyMLDSASignature( publicKeyPair, // Use the public-key-only keypair signed.message, signed.signature ); console.log('Signature valid:', isValid); // true // Verify with wrong message fails const isInvalid = MessageSigner.verifyMLDSASignature( publicKeyPair, 'Wrong message', signed.signature ); console.log('Invalid signature:', isInvalid); // false ``` **Important:** The `verifyMLDSASignature` method requires a keypair object, not just a raw public key. - **If you have the original keypair:** Use it directly (e.g., `wallet.mldsaKeypair`) - **If you only have the public key:** Use `QuantumBIP32Factory.fromPublicKey()` to reconstruct the keypair ### When to Use QuantumBIP32Factory.fromPublicKey() **Use it when you DON'T have the original keypair:** - Receiving a signature from someone else over the network - Verifying signatures from stored public keys in a database - Working with public keys in distributed systems - Validating signatures from external sources **Don't use it when you already have the keypair:** - Verifying your own signatures in the same session - Testing signatures you just created - When you have access to `wallet.mldsaKeypair` ### Creating a Public-Key-Only Keypair Parameters for `QuantumBIP32Factory.fromPublicKey()`: ```typescript const keypair = QuantumBIP32Factory.fromPublicKey( publicKey, // Uint8Array - ML-DSA public key (1312-2592 bytes) chainCode, // Uint8Array - Chain code (32 bytes) network, // Network - networks.bitcoin, networks.testnet, or networks.regtest securityLevel // MLDSASecurityLevel - LEVEL2, LEVEL3, or LEVEL5 ); ``` **Parameter Details:** - `publicKey`: The ML-DSA public key (1312 bytes for LEVEL2, 1952 for LEVEL3, 2592 for LEVEL5) - `chainCode`: BIP32 chain code (32 bytes) - available from `wallet.chainCode` - `network`: Bitcoin network configuration object - `securityLevel`: **Must match** the security level used to generate the original key **Why is this needed?** The `verifyMLDSASignature` method requires a keypair object (not just a raw public key) because: 1. It needs the security level information embedded in the keypair 2. It needs the proper key structure for the ML-DSA verification algorithm 3. It maintains consistency with BIP32 hierarchical deterministic key derivation ### Common Verification Scenarios **Scenario 1: Verifying your own signature (same session)** ```typescript const message = 'My message'; const signed = MessageSigner.signMLDSAMessage(wallet.mldsaKeypair, message); // You already have the keypair - use it directly const valid = MessageSigner.verifyMLDSASignature( wallet.mldsaKeypair, // Use existing keypair signed.message, signed.signature ); console.log('Valid:', valid); // true ``` **Scenario 2: Verifying a signature from someone else** ```typescript import { fromHex } from '@btc-vision/bitcoin'; // You receive these from the network/API: const receivedPublicKey = fromHex(/* hex string from network */); const receivedMessage = 'Message from sender'; const receivedSignature = fromHex(/* hex string from network */); const receivedChainCode = fromHex(/* hex string from network */); const receivedSecurityLevel = MLDSASecurityLevel.LEVEL2; // Reconstruct keypair from public key const keypair = QuantumBIP32Factory.fromPublicKey( receivedPublicKey, receivedChainCode, networks.bitcoin, receivedSecurityLevel ); // Verify the signature const valid = MessageSigner.verifyMLDSASignature( keypair, receivedMessage, receivedSignature ); console.log('Signature from other party valid:', valid); ``` **Scenario 3: Verifying stored signatures** ```typescript import { fromHex } from '@btc-vision/bitcoin'; // Load public key and signature from database const storedPublicKey = await db.getPublicKey(userId); const storedChainCode = await db.getChainCode(userId); const storedSecurityLevel = await db.getSecurityLevel(userId); const signature = await db.getSignature(messageId); const message = await db.getMessage(messageId); // Reconstruct keypair const keypair = QuantumBIP32Factory.fromPublicKey( fromHex(storedPublicKey), fromHex(storedChainCode), networks.bitcoin, storedSecurityLevel ); // Verify const valid = MessageSigner.verifyMLDSASignature( keypair, message, fromHex(signature) ); console.log('Stored signature valid:', valid); ``` ### Security Considerations **Chain Code:** - The chain code is public information in BIP32 - Store it alongside the public key for verification - It's not sensitive but required for keypair reconstruction **Security Level Matching:** - Always use the same security level for verification as was used for signing - Mismatched security levels will cause verification to fail - Store the security level with the public key **Network Matching:** - Ensure the network parameter matches the original signing network - Mainnet keys won't verify correctly if checked against testnet **Message Integrity:** - The message must match exactly between signing and verification - Even a single byte difference will cause verification to fail ## Schnorr Signing ### Basic Schnorr Signing Sign messages with classical Schnorr signatures: ```typescript import { MessageSigner } from '@btc-vision/transaction'; const wallet = mnemonic.derive(0); // Sign with Schnorr const message = 'Hello, Bitcoin!'; const signed = MessageSigner.signMessage(wallet.keypair, message); console.log('Message:', toHex(signed.message)); console.log('Signature:', toHex(signed.signature)); console.log('Signature Size:', signed.signature.length); // 64 bytes (Schnorr) ``` ### Verifying Schnorr Signatures ```typescript // Sign message const message = 'Verify this Schnorr signature'; const signed = MessageSigner.signMessage(wallet.keypair, message); // Verify signature (use the keypair's publicKey, not signed.publicKey which doesn't exist on SignedMessage) const isValid = MessageSigner.verifySignature( wallet.keypair.publicKey, signed.message, signed.signature ); console.log('Signature valid:', isValid); // true ``` ## Input Formats Both ML-DSA and Schnorr signing support multiple input formats: ### String Messages ```typescript // UTF-8 string const signed1 = MessageSigner.signMLDSAMessage(wallet.mldsaKeypair, 'Hello, World!'); // Any string content const signed2 = MessageSigner.signMLDSAMessage( wallet.mldsaKeypair, 'Emoji test: 🚀 Quantum 🔐' ); ``` ### Uint8Array Messages ```typescript // From UTF-8 string const message1 = new TextEncoder().encode('Hello, Uint8Array!'); const signed1 = MessageSigner.signMLDSAMessage(wallet.mldsaKeypair, message1); // Binary data const message2 = new Uint8Array([0x01, 0x02, 0x03, 0x04]); const signed2 = MessageSigner.signMLDSAMessage(wallet.mldsaKeypair, message2); // From hex import { fromHex } from '@btc-vision/bitcoin'; const message3 = fromHex('abcdef1234567890'); const signed3 = MessageSigner.signMLDSAMessage(wallet.mldsaKeypair, message3); ``` ### Uint8Array Messages ```typescript // Uint8Array const message = new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f]); // "Hello" const signed = MessageSigner.signMLDSAMessage(wallet.mldsaKeypair, message); ``` ### Hex String Messages ```typescript // Hex string (with 0x prefix) const signed1 = MessageSigner.signMLDSAMessage( wallet.mldsaKeypair, '0xdeadbeef' ); // Hex string (without 0x prefix) const signed2 = MessageSigner.signMLDSAMessage( wallet.mldsaKeypair, 'abcdef1234567890' ); ``` ### Cross-Format Verification Verification works across all input formats: ```typescript const message = 'Test message'; // Sign with string const signed = MessageSigner.signMLDSAMessage(wallet.mldsaKeypair, message); // Create public-key-only keypair for verification const publicKeyPair = QuantumBIP32Factory.fromPublicKey( signed.publicKey, wallet.chainCode, network, securityLevel ); // Verify with Uint8Array const messageBytes = new TextEncoder().encode(message); const valid1 = MessageSigner.verifyMLDSASignature( publicKeyPair, messageBytes, signed.signature ); // Verify with string directly const valid2 = MessageSigner.verifyMLDSASignature( publicKeyPair, message, signed.signature ); console.log(valid1 && valid2); // true - all formats work! ``` ## Tweaked Signatures ### Tweaked Schnorr Signing Sign with tweaked keys for Taproot compatibility: ```typescript import { MessageSigner } from '@btc-vision/transaction'; const wallet = mnemonic.derive(0); // Sign with tweaked key const message = 'Taproot message'; const signed = MessageSigner.tweakAndSignMessage(wallet.keypair, message); console.log('Tweaked Signature:', toHex(signed.signature)); console.log('Tweaked Public Key:', toHex(signed.publicKey)); ``` ### Verifying Tweaked Signatures ```typescript // Sign with tweak const message = 'Verify tweaked signature'; const signed = MessageSigner.tweakAndSignMessage(wallet.keypair, message); // Verify with tweak const isValid = MessageSigner.tweakAndVerifySignature( signed.publicKey, signed.message, signed.signature ); console.log('Tweaked signature valid:', isValid); // true ``` ## Message Hashing ### SHA-256 Hashing The MessageSigner automatically hashes messages before signing: ```typescript import { MessageSigner } from '@btc-vision/transaction'; // Long message const longMessage = 'This is a very long message that will be hashed before signing...'; // Automatically hashed to 32 bytes before signing const hash = MessageSigner.sha256(new TextEncoder().encode(longMessage)); console.log('Message hash:', toHex(hash)); console.log('Hash length:', hash.length); // 32 bytes // Then signed (signMLDSAMessage accepts string directly and hashes internally) const signed = MessageSigner.signMLDSAMessage(wallet.mldsaKeypair, longMessage); ``` ### Pre-hashed Messages ```typescript // You can also sign pre-hashed data const message = 'Original message'; const hash = MessageSigner.sha256(new TextEncoder().encode(message)); // Sign the hash directly (passing Uint8Array) const signed = MessageSigner.signMLDSAMessage(wallet.mldsaKeypair, hash); ``` ## Best Practices ### ✅ DO: ```typescript // Use appropriate security level for your use case const standardWallet = Mnemonic.generate( undefined, // Default strength (24 words) '', // No passphrase networks.bitcoin, // Mainnet MLDSASecurityLevel.LEVEL2 // Good for most applications ); // Include context in your messages const message = JSON.stringify({ action: 'transfer', amount: 1000, timestamp: Date.now(), nonce: crypto.randomBytes(16).toString('hex') }); // Verify signatures before trusting (first param is QuantumBIP32Interface keypair, not raw publicKey) const isValid = MessageSigner.verifyMLDSASignature( mldsaKeypair, message, signature ); if (!isValid) { throw new Error('Invalid signature'); } // Store signatures with metadata const signatureData = { message: signed.message, signature: toHex(signed.signature), publicKey: toHex(signed.publicKey), securityLevel: signed.securityLevel, timestamp: Date.now() }; ``` ### ❌ DON'T: ```typescript // Don't sign without verification MessageSigner.signMLDSAMessage(wallet.mldsaKeypair, userInput); // Dangerous! // Don't use signatures without checking validity // Always verify! // Don't expose private keys console.log(wallet.privateKey); // Never do this! // Don't sign arbitrary untrusted data const untrustedData = externalAPI.getData(); // Validate and sanitize first! // Don't reuse signatures for different messages // Generate new signature for each unique message ``` ### Message Structure ```typescript // Good: Structured, verifiable message interface SignedMessage { version: number; action: string; payload: any; timestamp: number; nonce: string; } const message: SignedMessage = { version: 1, action: 'authenticate', payload: { userId: '123' }, timestamp: Date.now(), nonce: crypto.randomBytes(16).toString('hex') }; const messageString = JSON.stringify(message); const signed = MessageSigner.signMLDSAMessage(wallet.mldsaKeypair, messageString); ``` ## Complete Example ```typescript import { MessageSigner, MLDSASecurityLevel, Mnemonic, QuantumBIP32Factory, } from '@btc-vision/transaction'; import { networks, toHex } from '@btc-vision/bitcoin'; const network = networks.regtest; const securityLevel = MLDSASecurityLevel.LEVEL2; // Setup const mnemonic = Mnemonic.generate(undefined, undefined, network, securityLevel); const wallet = mnemonic.derive(0); // 1. Sign with ML-DSA (Quantum-resistant) console.log('=== ML-DSA Signing ==='); const quantumMessage = 'Quantum-resistant message'; const quantumSigned = MessageSigner.signMLDSAMessage(wallet.mldsaKeypair, quantumMessage); console.log('Message:', quantumSigned.message); console.log('Signature Size:', quantumSigned.signature.length, 'bytes'); console.log('Public Key Size:', quantumSigned.publicKey.length, 'bytes'); console.log('Security Level:', quantumSigned.securityLevel); const keypair = QuantumBIP32Factory.fromPublicKey( quantumSigned.publicKey, wallet.chainCode, network, securityLevel, ); // Verify ML-DSA const quantumValid = MessageSigner.verifyMLDSASignature( keypair, quantumMessage, quantumSigned.signature, ); console.log('ML-DSA Valid:', quantumValid); // 2. Sign with Schnorr (Classical) console.log('\n=== Schnorr Signing ==='); const classicalMessage = 'Classical signature'; const classicalSigned = MessageSigner.signMessage(wallet.keypair, classicalMessage); console.log('Message:', classicalSigned.message); console.log('Signature Size:', classicalSigned.signature.length, 'bytes'); // Verify Schnorr const classicalValid = MessageSigner.verifySignature( wallet.keypair.publicKey, classicalMessage, classicalSigned.signature, ); console.log('Schnorr Valid:', classicalValid); // 3. Multiple Input Formats console.log('\n=== Input Format Tests ==='); const testMessage = 'Format test'; // String const sig1 = MessageSigner.signMLDSAMessage(wallet.mldsaKeypair, testMessage); // Uint8Array const sig2 = MessageSigner.signMLDSAMessage( wallet.mldsaKeypair, new TextEncoder().encode(testMessage), ); // All verify successfully console.log( 'String format valid:', MessageSigner.verifyMLDSASignature(wallet.mldsaKeypair, testMessage, sig1.signature), ); console.log( 'Uint8Array format valid:', MessageSigner.verifyMLDSASignature( wallet.mldsaKeypair, new TextEncoder().encode(testMessage), sig2.signature, ), ); ``` ## Auto Methods (CRITICAL - Browser/Backend Auto-Detection) > **This is the MOST important section for production applications.** Auto methods automatically detect whether you're running in a browser (with OP_WALLET extension) or backend (with local keypair) and call the correct underlying method. **ALWAYS use Auto methods unless you have an explicit reason not to.** ### Why Auto Methods Exist | Environment | Non-Auto Method | Auto Method | |-------------|----------------|-------------| | **Browser** (OP_WALLET) | `signMessage()` → **CRASHES** (no private key) | `signMessageAuto()` → uses OP_WALLET | | **Backend** (local keypair) | `signMessage()` → works | `signMessageAuto()` → uses local keypair | | **Browser** (no OP_WALLET) | `signMessage()` → **CRASHES** | `signMessageAuto()` → **throws clear error** | ### Environment Detection Flow ```mermaid flowchart TB A["signMessageAuto(message, keypair?)"] --> B{keypair provided?} B -->|Yes| C["Use local keypair<br/>(Backend path)"] B -->|No/null/undefined| D{OP_WALLET available?} D -->|Yes| E["Use OP_WALLET<br/>(Browser path)"] D -->|No| F["Throw Error:<br/>'No keypair provided and<br/>OP_WALLET is not available'"] style C fill:#4CAF50,color:white style E fill:#2196F3,color:white style F fill:#f44336,color:white ``` ### signMessageAuto Auto-detect environment and sign with Schnorr: ```typescript import { MessageSigner } from '@btc-vision/transaction'; // BROWSER: Pass no keypair → OP_WALLET signs const browserSigned = await MessageSigner.signMessageAuto('Hello, OPNet!'); // BACKEND: Pass keypair → local signing const backendSigned = await MessageSigner.signMessageAuto('Hello, OPNet!', wallet.keypair); ``` #### Signature ```typescript async signMessageAuto( message: Uint8Array | string, keypair?: UniversalSigner ): Promise<SignedMessage> ``` | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `message` | `Uint8Array \| string` | Yes | Message to sign | | `keypair` | `UniversalSigner` | No | Pass for backend; omit/null for browser (OP_WALLET) | ### tweakAndSignMessageAuto Auto-detect environment and sign with tweaked Schnorr (Taproot-compatible): ```typescript // BROWSER: OP_WALLET handles tweaking internally const browserSigned = await MessageSigner.tweakAndSignMessageAuto('Taproot message'); // BACKEND: Local tweaked signing (network required) const backendSigned = await MessageSigner.tweakAndSignMessageAuto( 'Taproot message', wallet.keypair, networks.bitcoin ); ``` #### Signature ```typescript async tweakAndSignMessageAuto( message: Uint8Array | string, keypair?: UniversalSigner, network?: Network ): Promise<SignedMessage> ``` | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `message` | `Uint8Array \| string` | Yes | Message to sign | | `keypair` | `UniversalSigner` | No | Pass for backend; omit for browser | | `network` | `Network` | Backend only | Required when keypair is provided | ### signMLDSAMessageAuto Auto-detect environment and sign with quantum-resistant ML-DSA: ```typescript // BROWSER: OP_WALLET handles ML-DSA signing const browserSigned = await MessageSigner.signMLDSAMessageAuto('Quantum message'); // BACKEND: Local ML-DSA signing const backendSigned = await MessageSigner.signMLDSAMessageAuto( 'Quantum message', wallet.mldsaKeypair ); ``` #### Signature ```typescript async signMLDSAMessageAuto( message: Uint8Array | string, mldsaKeypair?: QuantumBIP32Interface ): Promise<MLDSASignedMessage> ``` | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `message` | `Uint8Array \| string` | Yes | Message to sign | | `mldsaKeypair` | `QuantumBIP32Interface` | No | Pass for backend; omit for browser (OP_WALLET) | ### isOPWalletAvailable Check if OP_WALLET browser extension is available: ```typescript if (MessageSigner.isOPWalletAvailable()) { // Browser with OP_WALLET - can use Auto methods without keypair const signed = await MessageSigner.signMessageAuto('message'); } else { // Backend or browser without OP_WALLET - must provide keypair const signed = await MessageSigner.signMessageAuto('message', wallet.keypair); } ``` ### Complete Auto Method Example ```typescript import { MessageSigner, Mnemonic, MLDSASecurityLevel, } from '@btc-vision/transaction'; import { networks } from '@btc-vision/bitcoin'; /** * Universal signing function that works in both browser and backend. * In browser: pass no keypair/mldsaKeypair → OP_WALLET handles signing. * In backend: pass wallet keypairs → local signing. */ async function signForContract( message: string, keypair?: UniversalSigner, mldsaKeypair?: QuantumBIP32Interface, network?: Network, ): Promise<{ schnorr: SignedMessage; tweaked: SignedMessage; quantum: MLDSASignedMessage; }> { // All three Auto methods follow the same pattern: // - keypair provided → backend path (local signing) // - keypair omitted → browser path (OP_WALLET signing) const schnorr = await MessageSigner.signMessageAuto(message, keypair); const tweaked = await MessageSigner.tweakAndSignMessageAuto(message, keypair, network); const quantum = await MessageSigner.signMLDSAMessageAuto(message, mldsaKeypair); return { schnorr, tweaked, quantum }; } // BACKEND USAGE: const network = networks.regtest; const mnemonic = Mnemonic.generate(undefined, '', network, MLDSASecurityLevel.LEVEL2); const wallet = mnemonic.derive(0); const backendResult = await signForContract( 'Claim airdrop', wallet.keypair, wallet.mldsaKeypair, network, ); // BROWSER USAGE (in a React component, for example): // No keypair needed - OP_WALLET extension handles everything const browserResult = await signForContract('Claim airdrop'); ``` > **Rule of thumb:** If your code might run in both browser and backend, **ALWAYS use Auto methods**. The non-Auto methods (`signMessage`, `signMLDSAMessage`, `tweakAndSignMessage`) are environment-specific and will crash in the wrong context. --- ## OP_WALLET Integration Methods These methods are used internally by the Auto methods but can also be called directly: ### trySignSchnorrWithOPWallet ```typescript async trySignSchnorrWithOPWallet( message: Uint8Array | string ): Promise<SignedMessage | null> ``` Returns `null` if OP_WALLET is not available (safe to call in any environment). ### trySignMLDSAWithOPWallet ```typescript async trySignMLDSAWithOPWallet( message: Uint8Array | string ): Promise<MLDSASignedMessage | null> ``` Returns `null` if OP_WALLET is not available. ### verifyMLDSAWithOPWallet ```typescript async verifyMLDSAWithOPWallet( message: Uint8Array | string, signature: MLDSASignedMessage ): Promise<boolean | null> ``` Returns `null` if OP_WALLET is not available. ### getMLDSAPublicKeyFromOPWallet ```typescript async getMLDSAPublicKeyFromOPWallet(): Promise<Uint8Array | null> ``` Returns the ML-DSA public key from OP_WALLET, or `null` if unavailable. --- ## See Also - [Address Generation](./03-address-generation.md) - P2MR and P2TR address types, including quantum-safe P2MR outputs via `useP2MR` ## Next Steps - [Address Verification](./05-address-verification.md) - Validate addresses and public keys - [Introduction](./01-introduction.md) - Back to overview --- [← Previous: Address Generation](./03-address-generation.md) | [Next: Address Verification →](./05-address-verification.md)