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/OpnetTestnet/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, network.opnetTestnet 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
- Use appropriate security level.
- Include context in your messages.
- Verify signatures before trusting.
- Store signatures with metadata.
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
- Don't sign without verification.
- Don't use signatures without checking validity.
- Don't expose private keys.
- Don't sign arbitrary untrusted data.
- Don't reuse signatures for different messages.
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
Define a fixed, typed structure for the message payload that will be signed. This ensures consistency between the signing and verification steps, and prevents ambiguity in how the message data is serialized before hashing.
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,
),
);