MessageSigner Best Pratices

Recommendations

Default to Auto Methods

Unless there is a specific reason to use non-auto methods (such as synchronous signing in a guaranteed backend context), always prefer signMessageAuto(), tweakAndSignMessageAuto(), and signMLDSAMessageAuto(). They handle environment detection automatically and future-proof your code.

Always Verify Signatures

Never trust signed data without verification, especially data received over a network. Use verifySignature(), tweakAndVerifySignature(), or verifyMLDSASignature() before acting on signed messages.

Pass Original Messages to Verification

All verification methods hash the message internally with SHA-256. Pass the original message string or bytes, not a pre-computed hash. The only exception is if both signer and verifier explicitly agree on a pre-hashed protocol.

Use Untweaked Public Keys for Tweaked Verification

Pass the untweaked public key to tweakAndVerifySignature(). The method applies the Taproot tweak internally. Passing an already-tweaked key produces a double-tweaked key and verification will fail.

Provide Network for Tweaked Backend Signing

When calling tweakAndSignMessageAuto() or tweakAndSignMessage() with a local keypair, the network parameter is required for computing the correct Taproot tweak. Omitting it throws an error.

Reconstruct Keypairs for Remote ML-DSA Verification

Use QuantumBIP32Factory.fromPublicKey() for remote ML-DSA verification. When only a public key is available, reconstruct a public-key-only keypair before calling verifyMLDSASignature(). This is required because the verification algorithm needs security level metadata embedded in the keypair object.

Structure Signed Messages with Context

Include action type, timestamps, and nonces in signed messages to prevent replay attacks. Structured payloads ensure signatures cannot be reused in unintended contexts.

const message = JSON.stringify({
    action: 'transfer',
    amount: '50000',
    timestamp: Date.now(),
    nonce: toHex(crypto.getRandomValues(new Uint8Array(16))),
});

const signed = await MessageSigner.signMessageAuto(message, keypair);

Never Expose Private Keys

The OP_WALLET architecture exists specifically to keep private keys inside the browser extension. In backend code, load keys from secure storage (environment variables, HSM, encrypted vaults) and avoid logging them.

Handle OP_WALLET Absence Gracefully

In browser code, check isOPWalletAvailable() before signing to provide a user-friendly error message instead of an unhandled exception.

Match Security Levels for ML-DSA

When verifying ML-DSA signatures, the security level used for QuantumBIP32Factory.fromPublicKey() must match the level used during signing. Mismatched levels cause verification failure.