Message signing
OP_NET provides the MessageSigner class for all message signing operations. This class supports ML-DSA signing and verification for quantum-resistant cryptography, Schnorr signing and verification for classical operations, tweaked signatures for Taproot compatibility, and message hashing utilities.
ML-DSA Signing
Basic ML-DSA Signing
Use the signMLDSAMessage() method to generate quantum-resistant signatures using the ML-DSA algorithm.
import { Mnemonic, MessageSigner, MLDSASecurityLevel } from '@btc-vision/transaction';
import { networks } 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:', Buffer.from(signed.signature).toString('hex'));
console.log('Public Key:', Buffer.from(signed.publicKey).toString('hex'));
console.log('Security Level:', signed.securityLevel);ML-DSA Signature Sizes
ML-DSA signature sizes vary by security level:
- LEVEL2: Smallest signature size (2420 bytes), suitable for most applications.
- LEVEL3: Medium signature size (3309 bytes) with increased security.
- LEVEL5: Largest signature size (4627 bytes) with maximum quantum resistance.
import { Mnemonic, MessageSigner, MLDSASecurityLevel } from '@btc-vision/transaction';
import { networks } from '@btc-vision/bitcoin';
// 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 bytesVerifying ML-DSA Signatures
If required, use the QuantumBIP32Factory.fromPublicKey() method to create a public-key-only keypair, then call the MessageSigner.verifyMLDSASignature() method to verify the signature.
import { Mnemonic, MessageSigner, QuantumBIP32Factory } from '@btc-vision/transaction';
const mnemonic = Mnemonic.generate(undefined, '', networks.bitcoin, MLDSASecurityLevel.LEVEL2);
const wallet = mnemonic.derive(0);
// 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); // falseThe 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 this method when the original keypair is not available:
- Verifying signatures received from external sources over the network.
- Validating signatures using public keys stored in a database.
- Working with public keys in distributed systems.
- Processing signatures from third-party applications.
This method is not required when the keypair is already available:
- Verifying self-generated signatures within the same session.
- Testing signatures immediately after signing.
- When wallet.mldsaKeypair is available.
Creating a Public-Key-Only Keypair
Use the QuantumBIP32Factory.fromPublicKey() method to create a public-key-only keypair.
Required parameters:
- 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.
const keypair = QuantumBIP32Factory.fromPublicKey(
publicKey, // Uint8Array - ML-DSA public key (1312-2592 bytes)
chainCode, // Buffer - Chain code (32 bytes)
network, // Network - networks.bitcoin, networks.testnet, or networks.regtest
securityLevel // MLDSASecurityLevel - LEVEL2, LEVEL3, or LEVEL5
);Why Is This Required?
The verifyMLDSASignature() method requires a keypair object rather than a raw public key for the following reasons:
- The keypair contains the MLDSASecurityLevel information necessary for verification.
- The ML-DSA algorithm requires a specific key structure for signature verification.
- The keypair format ensures compatibility with BIP32 hierarchical deterministic key derivation.
Common Verification Scenarios
Scenario 1: Verifying self-generated signatures within the same session.import { Mnemonic, MessageSigner, MLDSASecurityLevel } from '@btc-vision/transaction';
import { networks } from '@btc-vision/bitcoin';
const mnemonic = Mnemonic.generate(undefined, '', networks.bitcoin, MLDSASecurityLevel.LEVEL2);
const wallet = mnemonic.derive(0);
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); // trueimport { Mnemonic, MessageSigner, MLDSASecurityLevel, QuantumBIP32Factory } from '@btc-vision/transaction';
import { networks } from '@btc-vision/bitcoin';
// You receive these from the network/API:
const receivedPublicKey = Buffer.from(/* hex string from network */);
const receivedMessage = 'Message from sender';
const receivedSignature = Buffer.from(/* hex string from network */);
const receivedChainCode = Buffer.from(/* 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);import { Mnemonic, MessageSigner, MLDSASecurityLevel,QuantumBIP32Factory } from '@btc-vision/transaction';
import { networks } 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(
Buffer.from(storedPublicKey, 'hex'),
Buffer.from(storedChainCode, 'hex'),
networks.bitcoin,
storedSecurityLevel
);
// Verify
const valid = MessageSigner.verifyMLDSASignature(
keypair,
message,
Buffer.from(signature, 'hex')
);
console.log('Stored signature valid:', valid);Security Considerations
Chain Code:
- The chain code is public information in BIP32.
- Store the chain Code alongside the public key for future 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 cause verification to fail.
- Store the security alongside the public key for future verification.
Network Matching:
- Ensure the network parameter matches the original signing network.
- Mainnet keys won't verify correctly if checked against another network.
Message Integrity:
- The message must be identical during signing and verification.
- Any modification, even a single byte, causes verification to fail.
Schnorr Signing
Basic Schnorr Signing
Use the signMessage() method to perform a classical Schnorr signature.
import { Mnemonic, MessageSigner } from '@btc-vision/transaction';
const mnemonic = Mnemonic.generate();
const wallet = mnemonic.derive(0);
// Sign with Schnorr
const message = 'Hello, Bitcoin!';
const signed = MessageSigner.signMessage(wallet.keypair, message);
console.log('Message:', signed.message);
console.log('Signature:', Buffer.from(signed.signature).toString('hex'));
console.log('Public Key:', Buffer.from(signed.publicKey).toString('hex'));
console.log('Signature Size:', signed.signature.length); // 64 bytes (Schnorr)Verifying Schnorr Signatures
Use the verifySignature() method to verify a classical Schnorr signature.
import { Mnemonic, MLDSASecurityLevel, MessageSigner } from '@btc-vision/transaction';
const mnemonic = Mnemonic.generate(undefined, '', networks.bitcoin, MLDSASecurityLevel.LEVEL2);
const wallet = mnemonic.derive(0);
// Sign message
const message = 'Verify this Schnorr signature';
const signed = MessageSigner.signMessage(wallet.keypair, message);
// Verify signature
const isValid = MessageSigner.verifySignature(
signed.publicKey,
signed.message,
signed.signature
);
console.log('Signature valid:', isValid); // trueInput Formats
Both ML-DSA and Schnorr signing method support multiple input formats.
String Messages
import { Mnemonic, MessageSigner } from '@btc-vision/transaction';
const mnemonic = Mnemonic.generate();
const wallet = mnemonic.derive(0);
// UTF-8 string
const signed1 = MessageSigner.signMLDSAMessage(wallet.mldsaKeypair, 'Hello, World!');
// Any string content
const signed2 = MessageSigner.signMLDSAMessage(
wallet.mldsaKeypair,
'Emoji test: 🚀 Quantum 🔐'
);
Buffer Messages
import { Mnemonic, MessageSigner } from '@btc-vision/transaction';
const mnemonic = Mnemonic.generate();
const wallet = mnemonic.derive(0);
// From UTF-8 string
const message1 = Buffer.from('Hello, Buffer!', 'utf-8');
const signed1 = MessageSigner.signMLDSAMessage(wallet.mldsaKeypair, message1);
// Binary data
const message2 = Buffer.from([0x01, 0x02, 0x03, 0x04]);
const signed2 = MessageSigner.signMLDSAMessage(wallet.mldsaKeypair, message2);
// From hex
const message3 = Buffer.from('abcdef1234567890', 'hex');
const signed3 = MessageSigner.signMLDSAMessage(wallet.mldsaKeypair, message3);Uint8Array Messages
import { Mnemonic, MessageSigner } from '@btc-vision/transaction';
const mnemonic = Mnemonic.generate();
const wallet = mnemonic.derive(0);
// Uint8Array
const message = new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f]); // "Hello"
const signed = MessageSigner.signMLDSAMessage(wallet.mldsaKeypair, message);Hex String Messages
import { Mnemonic, MessageSigner } from '@btc-vision/transaction';
const mnemonic = Mnemonic.generate();
const wallet = mnemonic.derive(0);
// 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.
import { Mnemonic, MessageSigner, QuantumBIP32Factory } from '@btc-vision/transaction';
const mnemonic = Mnemonic.generate();
const wallet = mnemonic.derive(0);
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 Buffer
const messageBuffer = Buffer.from(message, 'utf-8');
const valid1 = MessageSigner.verifyMLDSASignature(
publicKeyPair,
messageBuffer,
signed.signature
);
// Verify with Uint8Array
const messageArray = new Uint8Array(Buffer.from(message, 'utf-8'));
const valid2 = MessageSigner.verifyMLDSASignature(
publicKeyPair,
messageArray,
signed.signature
);
console.log(valid1 && valid2); // true - all formats work!Tweaked Signatures
Tweaked Schnorr Signing
Use the tweakAndSignMessage() method to sign with tweaked keys for Taproot compatibility.
import { Mnemonic, MessageSigner } from '@btc-vision/transaction';
const mnemonic = Mnemonic.generate();
const wallet = mnemonic.derive(0);
// Sign with tweaked key
const message = 'Taproot message';
const signed = MessageSigner.tweakAndSignMessage(wallet.keypair, message);
console.log('Tweaked Signature:', Buffer.from(signed.signature).toString('hex'));
console.log('Tweaked Public Key:', Buffer.from(signed.publicKey).toString('hex'));Verifying Tweaked Signatures
Use the tweakAndVerifySignature() method to verify a message signed with tweakAndSignMessage().
import { Mnemonic, MessageSigner } from '@btc-vision/transaction';
const mnemonic = Mnemonic.generate();
const wallet = mnemonic.derive(0);
// 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); // trueMessage Hashing
SHA-256 Hashing
The MessageSigner automatically hashes messages before signing.
Pre-hashed Messages
The MessageSigner class also supports signing pre-hashed messages.
import { Mnemonic, MessageSigner } from '@btc-vision/transaction';
const mnemonic = Mnemonic.generate();
const wallet = mnemonic.derive(0);
// You can also sign pre-hashed data
const message = 'Original message';
const hash = MessageSigner.sha256(message);
// Sign the hash directly
const signed = MessageSigner.signMLDSAMessage(wallet.mldsaKeypair, hash);Security Best Practices
Good Practices
import { Mnemonic, MessageSigner, MLDSASecurityLevel } from '@btc-vision/transaction';
// Use appropriate security level for your use case
const standardWallet = Mnemonic.generate(
undefined, // Default strength (12 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
const isValid = MessageSigner.verifyMLDSASignature(
publicKey,
message,
signature
);
if (!isValid) {
throw new Error('Invalid signature');
}
// Store signatures with metadata
const signatureData = {
message: signed.message,
signature: Buffer.from(signed.signature).toString('hex'),
publicKey: Buffer.from(signed.publicKey).toString('hex'),
securityLevel: signed.securityLevel,
timestamp: Date.now()
};Bad Practices
import { Mnemonic, MessageSigner, MLDSASecurityLevel } from '@btc-vision/transaction';
const mnemonic = Mnemonic.generate();
const wallet = mnemonic.derive(0);
// 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 messageMessage Structure
import { Mnemonic, MessageSigner, MLDSASecurityLevel } from '@btc-vision/transaction';
const mnemonic = Mnemonic.generate();
const wallet = mnemonic.derive(0);
// 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
import {
MessageSigner,
MLDSASecurityLevel,
Mnemonic,
QuantumBIP32Factory,
} from '@btc-vision/transaction';
import { networks } 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);
// Buffer
const sig2 = MessageSigner.signMLDSAMessage(wallet.mldsaKeypair, Buffer.from(testMessage, 'utf-8'));
// Uint8Array
const sig3 = MessageSigner.signMLDSAMessage(
wallet.mldsaKeypair,
new Uint8Array(Buffer.from(testMessage, 'utf-8')),
);
// All verify successfully
console.log(
'String format valid:',
MessageSigner.verifyMLDSASignature(wallet.mldsaKeypair, testMessage, sig1.signature),
);
console.log(
'Buffer format valid:',
MessageSigner.verifyMLDSASignature(
wallet.mldsaKeypair,
Buffer.from(testMessage),
sig2.signature,
),
);
console.log(
'Uint8Array format valid:',
MessageSigner.verifyMLDSASignature(
wallet.mldsaKeypair,
new Uint8Array(Buffer.from(testMessage)),
sig3.signature,
),
);