http://docker:8085Generate ML-DSA-65 keypair. Wallet address derived from SHA-256 of public key (0x-prefixed, 20 bytes).
ML-DSA-65 (FIPS 204)Sign transaction data with the wallet's private key using provider.sign(). Signature proves the wallet owner authorized the transaction.
ML-DSA-65 (FIPS 204)Verify any transaction signature against the signer's public key using provider.verify(). Anyone with the public key can verify.
ML-DSA-65 (FIPS 204)This service demonstrates quantum-safe wallet operations — the same patterns you'll implement using the Qudo JNI provider in your own wallet/blockchain system.
Create wallet: provider.generateKeyPair("ML-DSA-65") — address derived from SHA-256 of public key
Sign transaction: provider.sign(txData, privKey, "ML-DSA-65")
Verify transaction: provider.verify(txData, sig, pubKey, "ML-DSA-65")
/api/wallet/create
Create a PQC wallet
/api/wallet/{id}/sign-transaction
Sign a transaction with wallet's private key
/api/wallet/verify-transaction
Verify a transaction signature
/api/wallet/list
List all wallets
/api/wallet/health
Service health + wallet count
Migrate your wallet/blockchain system from classical ECDSA (secp256k1) to quantum-safe ML-DSA-65. Use this playground to try PQC wallet operations, then use the Qudo JNI provider directly in your own wallet infrastructure.
Wallet Owner Verifier / Network
| |
| 1. provider.generateKeyPair("ML-DSA-65") |
| -> private key (stored securely) |
| -> public key -> SHA-256 -> wallet address (0x...)|
| |
| 2. provider.sign(txData, privKey, "ML-DSA-65") |
| -> 3,309-byte signature |
| |
|---- transaction + signature + public key ----------->|
| |
| 3. provider.verify(txData, sig, pubKey, |
| "ML-DSA-65") -> true/false |
| 4. Derive address from pubKey, match sender
The wallet address is derived from SHA-256(publicKey), same pattern as classical wallets but with a quantum-safe keypair. The private key signs transactions; the public key verifies them.
Each transaction is signed with provider.sign(txData, privKey, "ML-DSA-65"). The 3,309-byte signature proves the wallet owner authorized the transaction. Replaces ECDSA secp256k1.
Replace ECDSA secp256k1 with ML-DSA-65 in your wallet system. The transaction signing pattern stays the same — only the crypto library changes.
// Classical wallet
ECGenParameterSpec spec =
new ECGenParameterSpec("secp256k1");
KeyPairGenerator kpg =
KeyPairGenerator.getInstance("EC");
kpg.initialize(spec);
KeyPair kp = kpg.generateKeyPair();
// Sign tx
Signature signer =
Signature.getInstance("SHA256withECDSA");
signer.initSign(kp.getPrivate());
signer.update(txData);
byte[] sig = signer.sign(); // 64 bytes
// PQC wallet
QudoKeyPair kp = provider.generateKeyPair(
"ML-DSA-65");
// Derive address (same pattern)
byte[] addr = provider.sha256(
kp.getPublicKeyPem());
// Sign tx
byte[] sig = provider.sign(txData,
kp.getPrivateKeyPem(),
"ML-DSA-65"); // 3,309 bytes
// Verify
boolean ok = provider.verify(txData,
sig, kp.getPublicKeyPem(),
"ML-DSA-65");
import com.qudo.crypto.QudoCrypto;
import com.qudo.crypto.QudoKeyPair;
QudoCrypto provider = QudoCrypto.create();
// ============================================================
// 1. Create wallet (generate keypair + derive address)
// ============================================================
QudoKeyPair walletKeys = provider.generateKeyPair("ML-DSA-65");
byte[] addrHash = provider.sha256(walletKeys.getPublicKeyPem());
// Address = first 20 bytes of SHA-256, hex-encoded, 0x-prefixed
StringBuilder sb = new StringBuilder("0x");
for (int i = 0; i < 20; i++) sb.append(String.format("%02x", addrHash[i]));
String address = sb.toString(); // e.g., "0x08df2a5997394d45..."
// Store walletKeys.getPrivateKeyPem() securely (HSM, keystore)
// Publish walletKeys.getPublicKeyPem() + address
// ============================================================
// 2. Sign a transaction
// ============================================================
byte[] txData = "transfer 100 tokens to 0xabc123".getBytes();
byte[] signature = provider.sign(txData, walletKeys.getPrivateKeyPem(), "ML-DSA-65");
// Broadcast: txData + signature + publicKey
// ============================================================
// 3. Verify a transaction (anyone can do this)
// ============================================================
boolean valid = provider.verify(txData, signature,
walletKeys.getPublicKeyPem(), "ML-DSA-65");
// valid == true -> transaction is authorized by wallet owner
// ============================================================
// 4. Verify sender address matches (on the verifier side)
// ============================================================
// senderPublicKey = public key received with the transaction
byte[] senderHash = provider.sha256(senderPublicKey);
StringBuilder senderSb = new StringBuilder("0x");
for (int i = 0; i < 20; i++) senderSb.append(String.format("%02x", senderHash[i]));
String senderAddress = senderSb.toString();
// Compare senderAddress with the claimed sender in txData
provider.close();
| Component | Classical (ECDSA) | PQC (Qudo JNI) |
|---|---|---|
| Keypair generation | secp256k1 | provider.generateKeyPair("ML-DSA-65") |
| Address derivation | Keccak256(pubKey) | provider.sha256(pubKey) — same pattern |
| Transaction signing | SHA256withECDSA | provider.sign(tx, key, "ML-DSA-65") |
| Signature verification | ECDSA verify | provider.verify(tx, sig, key, "ML-DSA-65") |
| Signature size | 64 bytes | 3,309 bytes (~50x larger) |
| Transaction format | Same | Same — just larger signature field |
| Address format | Same (0x-prefixed) | Same |
| Operation | PQC (ML-DSA-65) | Classical (ECDSA) |
|---|---|---|
| Keypair generation | ~55ms | <1ms |
| Transaction sign | ~10ms (cached key) | <1ms |
| Signature verify | ~5ms | <1ms |
| Signature size | 3,309 bytes | 64 bytes |
| Public key size | ~2,726 bytes (PEM) | 33 bytes (compressed) |
Cause: ML-DSA-65 signatures are 3,309 bytes vs 64 bytes for ECDSA. A block with 1,000 transactions adds ~3.3MB of signature data.
Mitigation: Use ML-DSA-44 (2,420 bytes) for higher throughput. Or aggregate signatures off-chain and commit a batch proof on-chain.
Cause: ML-DSA-65 public keys are ~2.7KB vs 33 bytes for compressed ECDSA. Address books and peer discovery need larger storage.
Fix: Use the wallet address (20 bytes) for identification. Only transmit the full public key when verifying a transaction.
Cause: ML-DSA-65 private keys are larger than ECDSA. Existing keystores may need capacity updates.
Fix: The Qudo provider outputs standard PEM keys. Store with provider.generateKeyPair("ML-DSA-65").getPrivateKeyPem() in your existing HSM or keystore.
A: Not directly — the signature algorithms are incompatible. During migration, run dual-signature wallets: sign with both ECDSA and ML-DSA-65. Verifiers check whichever they support. Phase out ECDSA once the network migrates.
A: ML-DSA-44 (2,420-byte signatures, ~8ms sign). For most wallets, ML-DSA-65 is recommended (3,309 bytes, ~10ms). Only use ML-DSA-87 for high-value cold storage wallets.
A: Same pattern as classical: address = SHA-256(publicKey)[0:20]. The public key is larger (2.7KB vs 33 bytes) but the derived address is the same size (20 bytes, 0x-prefixed). Clients identify wallets by address, not public key.
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.