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.

typescript
How to use signMLDSAMessage()
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.
typescript
Different signature sizes
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 bytes

Verifying 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.

typescript
How to verify a ML-DSA 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);  // 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 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.
typescript
QuantumBIP32Factory.fromPublicKey() required parameters
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.
typescript
How to use existing keypair to verify signature
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);  // true
Scenario 2: Verifying a signature from an external source.
typescript
How to use QuantumBIP32Factory.fromPublicKey() to verify a signature
import { 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);
Scenario 3: Verifying stored signatures.
typescript
How to use QuantumBIP32Factory.fromPublicKey() to verify a signature
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.

typescript
How to use signMessage()
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.

typescript
How to use verifySignature()
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);  // true

Input Formats

Both ML-DSA and Schnorr signing method support multiple input formats.

String Messages

typescript
How to sign a string message
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

typescript
How to sign a buffer message
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

typescript
How to sign a Uint8Array message
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

typescript
How to sign a hex string message
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.

typescript
How the verification works across all format
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.

typescript
How to use tweakAndSignMessage()
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().

typescript
How to use tweakAndVerifySignature()
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);  // true

Message Hashing

SHA-256 Hashing

The MessageSigner automatically hashes messages before signing.

Pre-hashed Messages

The MessageSigner class also supports signing pre-hashed messages.

typescript
How to sign a pre-hashed message
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.
typescript
Appropriate usage
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.
typescript
Don't sign
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 message

Message 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.

typescript
Sing typed message
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

typescript
How to use all supported signing methods
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,
    ),
);