http://docker:8085Deploy smart contracts with ML-DSA-65 signature over the code hash. Quantum-safe provenance from day one.
ML-DSA-65 (FIPS 204)Every transaction signed with both ECDSA (secp256r1) and ML-DSA-65. Legacy clients verify ECDSA; PQC clients verify ML-DSA-65.
ECDSA + ML-DSA-65Verify both signatures. Network enforces bothValid during transition; phases out ECDSA once all clients are PQC-capable.
ECDSA + ML-DSA-65This service demonstrates the dual-signature pattern for smart contracts — the migration bridge between classical and post-quantum blockchains.
Sign once, verify anywhere: Every transaction gets both an ECDSA signature (for legacy verifiers) and an ML-DSA-65 signature (for PQC-aware verifiers). Verifiers check whichever signature they support.
Classical (ECDSA secp256r1): Java's Signature.getInstance("SHA256withECDSA")
PQC (ML-DSA-65): provider.sign(data, privKey, "ML-DSA-65") via Qudo JNI
Contracts are deployed with an ML-DSA-65 signature over the SHA-256 code hash, establishing quantum-safe provenance.
/api/dapp/deploy-contract
Deploy a smart contract with PQC signature
/api/dapp/dual-sign
Dual-sign transaction (ECDSA + ML-DSA-65)
/api/dapp/dual-verify
Verify both ECDSA and ML-DSA-65 signatures
/api/dapp/contracts
List all deployed contracts
/api/dapp/health
Service health + supported algorithms
Migrate your smart contracts to quantum-safe signatures using the dual-signature pattern: keep ECDSA for legacy verifiers while adding ML-DSA-65 for PQC-aware verifiers. Use this playground to try dual signing, then use the Qudo JNI provider in your own dApp.
dApp Client Verifier Network
| |
| 1. Sign the SAME txData bytes with BOTH keys |
| ecSig = Signature.getInstance("SHA256withECDSA") |
| .sign(txData) // secp256r1 |
| mlSig = provider.sign(txData, mldsaPrivKey, |
| "ML-DSA-65") |
| |
|---- txData + ecSig + mlSig + both public keys ------>|
| |
| 2. Legacy verifier checks ecSig (ECDSA) |
| PQC verifier checks mlSig (ML-DSA-65) |
| Strict verifier checks BOTH |
| |
| 3. Migration phases: |
| Phase 1 (bootstrap): accept if ecValid |
| Phase 2 (transition): require bothValid |
| Phase 3 (PQC only): accept if mlValid
Legacy clients that only know ECDSA continue to work. They verify ecdsaSignature and ignore the ML-DSA field. No hard fork required.
Once a majority of verifiers are PQC-capable, switch to bothValid enforcement. Attackers breaking ECDSA with a quantum computer still can't forge ML-DSA-65.
Add the Qudo JNI provider alongside your existing ECDSA code. Keep both signatures on every transaction until the network fully migrates.
import com.qudo.crypto.QudoCrypto;
import com.qudo.crypto.QudoKeyPair;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Signature;
import java.security.spec.ECGenParameterSpec;
// ============================================================
// Initialize once at startup
// ============================================================
QudoCrypto provider = QudoCrypto.create();
// Classical ECDSA keypair (existing code, unchanged)
KeyPairGenerator ecKpg = KeyPairGenerator.getInstance("EC");
ecKpg.initialize(new ECGenParameterSpec("secp256r1"));
KeyPair ecKeys = ecKpg.generateKeyPair();
// PQC ML-DSA-65 keypair (new)
QudoKeyPair mldsaKeys = provider.generateKeyPair("ML-DSA-65");
// ============================================================
// Sign a transaction with BOTH algorithms
// ============================================================
byte[] txData = "transfer 100 PQC to 0xabc123".getBytes();
// Classical ECDSA (unchanged)
Signature ecSigner = Signature.getInstance("SHA256withECDSA");
ecSigner.initSign(ecKeys.getPrivate());
ecSigner.update(txData);
byte[] ecSig = ecSigner.sign(); // 64-72 bytes
// PQC ML-DSA-65 via Qudo JNI (MUST sign the SAME txData bytes)
byte[] mlSig = provider.sign(txData, mldsaKeys.getPrivateKeyPem(), "ML-DSA-65"); // 3,309 bytes
// Broadcast: txData + ecSig + mlSig + both public keys (ecKeys.getPublic(), mldsaKeys.getPublicKeyPem())
// The verifier is a separate system — it receives the public keys over the wire.
// ============================================================
// Verify both signatures (on the verifier side)
// ============================================================
// ECDSA verification (unchanged)
Signature ecVerifier = Signature.getInstance("SHA256withECDSA");
ecVerifier.initVerify(ecKeys.getPublic());
ecVerifier.update(txData);
boolean ecValid = ecVerifier.verify(ecSig);
// ML-DSA-65 verification via Qudo JNI
boolean mlValid = provider.verify(txData, mlSig, mldsaKeys.getPublicKeyPem(), "ML-DSA-65");
// Migration policy
boolean bothValid = ecValid && mlValid;
// Phase 1 (now): accept if ecValid
// Phase 2 (transition): require bothValid
// Phase 3 (PQC only): accept if mlValid
provider.close();
| Component | Changes? | What to do |
|---|---|---|
| Transaction schema | Yes — add fields | Add mldsaSignature and mldsaPublicKey alongside existing ECDSA fields |
| Signing logic | Yes — add call | Add provider.sign(data, privKey, "ML-DSA-65") after your ECDSA sign |
| Verification logic | Yes — add check | Add provider.verify(). Enforce bothValid during transition. |
| ECDSA code | No | Keep as-is — legacy verifiers depend on it |
| Smart contract bytecode | No | Dual signatures are at the transaction layer, not contract layer |
| Wallet addresses | No | Addresses stay the same. New ML-DSA public keys published alongside existing ECDSA keys. |
Numbers assume keypairs are cached at startup (generated once, reused for all transactions).
| Operation | ECDSA secp256r1 | ML-DSA-65 | Dual (both) |
|---|---|---|---|
| Keypair generation (once at startup) | ~3ms | ~55ms | ~58ms startup |
| Sign (per transaction) | <1ms | ~10ms | ~11ms total |
| Verify (per transaction) | <1ms | ~5ms | ~6ms total |
| Signature size | 64-72 bytes | 3,309 bytes | ~3.4KB per tx |
| Public key size | 33 bytes (compressed) | ~2,726 bytes (PEM) | ~2.8KB per wallet |
Cause: Adding ML-DSA-65 to every transaction adds ~3.3KB per tx. A block with 1,000 txs grows by ~3.3MB.
Mitigation: Use ML-DSA-44 (2,420 bytes) for higher throughput, or aggregate PQC signatures off-chain with a commitment on-chain.
Cause: If ECDSA signs txDataV1 and ML-DSA signs txDataV2, an attacker who breaks one algorithm can swap in a different transaction under the other signature.
Fix: Sign the exact same bytes with both algorithms. Serialize the transaction (including nonce, chain ID, timestamp) into one canonical txData array, then sign that array twice.
Cause: An attacker strips the ML-DSA signature from a transaction, forges an ECDSA signature (using a quantum computer against the weaker algorithm), and submits it. A permissive verifier that accepts either signature would let this through.
Fix: Enforce bothValid (Phase 2) as soon as most wallets support ML-DSA. Never accept transactions with only one signature once PQC keys are registered.
A: A hard fork to PQC-only signatures breaks all existing wallets. Dual signatures let the network migrate gradually: old wallets still work (ECDSA), new wallets add PQC protection, and the network enforces stricter policies over time.
A: No. Dual signatures are at the transaction envelope layer, not the contract bytecode. Your Solidity/Vyper contracts stay identical. Only the transaction signing and verification layers change.
A: Match your chain's native curve. Ethereum/Bitcoin use secp256k1; enterprise chains and this simulator use secp256r1 (NIST P-256). The Qudo JNI provider adds ML-DSA-65 alongside either curve.
A: When >80% of active wallets on your network have ML-DSA keys. Until then, enforcing bothValid would break legacy users. After 80%, require both; after 95%, consider PQC-only.
Analyze any endpoint's cryptographic configuration. Enter a host:port to scan its TLS setup and identify what's quantum-safe vs what needs migration.