Using MessageSigner

Architecture Overview

The MessageSigner auto-detection flow determines which signing backend to use at runtime:

Message Signing Flow signMessageAuto(message, keypair?) keypair provided? Yes Local Signing (Backend) No / undefined check fallback... OP_WALLET available? Yes OP_WALLET Signing (Browser Extension) No Throw Error SignedMessage

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.

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.

Dual Environment Support

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

Unified Signing with Auto Methods

The Auto methods (signMessageAuto, tweakAndSignMessageAuto, signMLDSAMessageAuto) solve this by cheking if a keypair is provided.

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.

// 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).

// 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

// 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

// 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

// 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

typescript
Backend Schnorr Signing
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
typescript
Backend Tweaked Signing
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
typescript
Backend ML-DSA Signing
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
Browser Auto Signing
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
Universal Code with Auto Methods
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
Full Verification Workflow
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');
}