ML-DSA Post-Quantum Digital Signatures (Family Root)
NIST FIPS 204 ML-DSA post-quantum digital signature implementation for Lux Network. Family root for parameter tiers LP-4410 (ML-DSA-44), LP-4420 (ML-DSA-65, default), LP-4430 (ML-DSA-87).
Implementation status (code-audited 2026-07-03): PARTIAL Core ML-DSA wrapper confirmed in luxfi/crypto/mldsa (mldsa.go) with EVM precompile; sizes corrected to FIPS 204 final; default signing is the hedged FIPS 204 variant (
SignCtx, mldsa.go:182) withSignCtxDeterministicas opt-in; kat_test.go uses self-generated round-trip vectors, not NIST ACVP vectors.
Provenance
Originally LP-4316; renumbered for the unified-4xxx consolidation (PQ lattice sigs 4400-4499; family-root). Parameter tiers: LP-4410 (ML-DSA-44), LP-4420 (ML-DSA-65, network default), LP-4430 (ML-DSA-87). The 4316 slot is a permanent pointer to this LP.
Abstract
ML-DSA (Module-Lattice-Based Digital Signature Algorithm, NIST FIPS 204) integration for Lux Network. Quantum-resistant digital signature for consensus and transaction signing.
Specification
Algorithm
Fiat-Shamir with Aborts construction over module lattices.
Security: MLWE (Module Learning With Errors). Ring polynomials mod
q = 8380417. Parameters: d (dimension), η (noise), γ (challenge
weight).
Security Levels
| Mode | Security | Public Key | Private Key | Signature | Sign | Verify |
|---|---|---|---|---|---|---|
| ML-DSA-44 | 128-bit (NIST-2) | 1,312 B | 2,560 B | 2,420 B | ~150μs | ~80μs |
| ML-DSA-65 | 192-bit (NIST-3) | 1,952 B | 4,032 B | 3,309 B | ~417μs | ~108μs |
| ML-DSA-87 | 256-bit (NIST-5) | 2,592 B | 4,896 B | 4,627 B | ~600μs | ~150μs |
Lux default: ML-DSA-65.
Key Generation
import "github.com/luxfi/crypto/mldsa"
sk, err := mldsa.GenerateKey(rand.Reader, mldsa.MLDSA65)
pk := sk.PublicKey
privBytes := sk.Bytes() // 4,032 B
pubBytes := pk.Bytes() // 1,952 B
Signing (hedged default; deterministic opt-in; no k-value)
The shipped default (SignCtx) is the hedged (randomized) FIPS 204
variant; a deterministic variant is available as the separate opt-in
SignCtxDeterministic. Both remove ECDSA's k-value fragility.
signature, err := sk.Sign(rand.Reader, message, nil)
// 3,309 B for ML-DSA-65
Verification
valid := pk.Verify(message, signature, nil)
Checks: signature size (3,309 B for ML-DSA-65), polynomial bounds, challenge reconstruction.
Integration
P-Chain Validators (Hybrid BLS + ML-DSA)
type ValidatorSignature struct {
BLS []byte // 96 B
MLDSA []byte // 3,309 B
Mode uint8 // 44 / 65 / 87
}
Both signatures must be valid. Gradually shift weight from BLS to ML-DSA over the transition.
Transaction Signing
Address: lux1mldsa<mode><bech32-encoded-pubkey-hash> (e.g.
lux1mldsa65qpr3zvr8j5y5jxm9d8qgtnpwjx7h9k2v).
type MLDSATransaction struct {
ChainID ids.ID
Nonce uint64
To common.Address
Value *big.Int
Data []byte
Signature []byte // 3,309 B (ML-DSA-65)
PublicKey []byte // 1,952 B
Mode uint8 // 65
}
EVM Precompile
Address: 0x0000000000000000000000000000000000012202
interface IMLDSA {
function verify(
bytes calldata publicKey,
bytes calldata message,
bytes calldata signature
) external view returns (bool valid);
}
Gas: 100,000 base + 10/byte of message.
contract SecureVault {
address constant MLDSA = 0x0000000000000000000000000000000000012202;
function withdraw(
bytes calldata pubKey,
bytes calldata message,
bytes calldata signature
) external {
(bool success, bytes memory result) = MLDSA.staticcall(
abi.encode(pubKey, message, signature)
);
require(success && abi.decode(result, (bool)), "Invalid signature");
}
}
Implementation
Core Library
github.com/luxfi/crypto/mldsa
(local: ~/work/lux/crypto/mldsa/).
Files: mldsa.go (core, 7,687 B), mldsa_test.go (7,480 B), README.md.
Dep: github.com/cloudflare/circl v1.6.3 (FIPS 204).
package mldsa
type Mode int
const (
MLDSA44 Mode = iota // 128-bit
MLDSA65 // 192-bit (default)
MLDSA87 // 256-bit
)
type PrivateKey struct { /* ... */ }
type PublicKey struct { /* ... */ }
func GenerateKey(rand io.Reader, mode Mode) (*PrivateKey, error)
func (sk *PrivateKey) Sign(rand io.Reader, message []byte, opts crypto.SignerOpts) ([]byte, error)
func (pk *PublicKey) Verify(message, signature []byte, opts crypto.SignerOpts) bool
func PrivateKeyFromBytes(mode Mode, data []byte) (*PrivateKey, error)
func PublicKeyFromBytes(data []byte, mode Mode) (*PublicKey, error)
EVM Precompile
github.com/luxfi/evm/precompile/contracts/mldsa
(local: ~/work/lux/evm/precompile/contracts/mldsa/).
Precompile address 0x0000000000000000000000000000000000012202.
Files: contract.go (4,477 B), contract_test.go (7,505 B),
module.go (1,132 B), IMLDSA.sol (7,070 B), README.md (5,486 B).
Test Results
11/11 passing: SignVerify, InvalidSignature, WrongMessage,
EmptyMessage, LargeMessage, PrivateKeyFromBytes,
PublicKeyFromBytes, InvalidMode, InvalidKeySize,
GetPublicKeySize, GetSignatureSize.
Benchmarks (Apple M1 Max):
BenchmarkMLDSA_Sign_65 2,400 ops 417,000 ns/op
BenchmarkMLDSA_Verify_65 9,259 ops 108,000 ns/op
BenchmarkMLDSA_KeyGen_65 8,000 ops 125,000 ns/op
Migration
Phase 1 — validator support: ML-DSA public keys in validator registration; hybrid BLS + ML-DSA signing; gradual weight shift. Phase 2 — transaction support: deploy ML-DSA precompile on C-Chain; enable ML-DSA transaction signing; wallet integration. Phase 3 — full transition: ML-DSA primary; BLS for backwards compatibility; new validators require ML-DSA keys.
Security Considerations
Quantum resistance. Based on MLWE; no known efficient quantum algorithms. NIST analyzed for 6+ years before standardization. Conservative parameters (128/192/256-bit).
Side-channel. All arithmetic constant-time. No secret-dependent branches or memory access. CIRCL library production-validated.
Key management. Generate fresh per validator; store in HSM; separate consensus vs transaction keys. ML-DSA private key is 4,032 B (~65% larger than ECDSA). BIP-39 seed derivation; encrypt backups with AES-256.
Backwards Compatibility
Hybrid period (2026-2027): validators support both BLS and ML-DSA; transactions can use ECDSA or ML-DSA; consensus requires both signature types valid. ECDSA addresses keep functioning. Gradual deprecation over 2-3 years.
Rationale
ML-DSA vs SLH-DSA: 10-100x faster (417μs vs 40ms), smaller sigs (3,309 B vs 17,088-49,856 B); trade: lattice vs hash-only security. ML-DSA vs Falcon: simpler (no floating point), more conservative parameters, hedged signing by default with a deterministic opt-in.
ML-DSA-65 as default: 192-bit NIST Level 3 exceeds Bitcoin's 128-bit. 417μs sign / 108μs verify acceptable for blockchain. 3,309 B sigs fit typical network packets.
Reference
package main
import (
"crypto/rand"
"fmt"
"github.com/luxfi/crypto/mldsa"
)
func main() {
sk, _ := mldsa.GenerateKey(rand.Reader, mldsa.MLDSA65)
blockHash := []byte("block_hash_data_here")
signature, _ := sk.Sign(rand.Reader, blockHash, nil)
fmt.Printf("Signature size: %d bytes\n", len(signature)) // 3309
valid := sk.PublicKey.Verify(blockHash, signature, nil)
fmt.Printf("Signature valid: %v\n", valid) // true
}
Copyright
Copyright and related rights waived via CC0.
References
Related LPs
- LP-4500 — SLH-DSA (hash-based alternative)
- LP-4600 — ML-KEM (complementary KEM)
- LP-4940 — Hybrid envelope
Standards
- FIPS 204: Module-Lattice-Based Digital Signature Standard
- CRYSTALS-Dilithium: Spec v3.1
- CIRCL: Cloudflare Cryptographic Library
Implementation
~/work/lux/crypto/mldsa/~/work/lux/evm/precompile/contracts/mldsa/
Appendix A: Size comparison
| Scheme | Public Key | Private Key | Signature | Security |
|---|---|---|---|---|
| ECDSA (secp256k1) | 33 B | 32 B | 65 B | 128-bit (classical) |
| BLS12-381 | 96 B | 32 B | 96 B | 128-bit (classical) |
| ML-DSA-44 | 1,312 B | 2,560 B | 2,420 B | 128-bit (quantum) |
| ML-DSA-65 | 1,952 B | 4,032 B | 3,309 B | 192-bit (quantum) |
| ML-DSA-87 | 2,592 B | 4,896 B | 4,627 B | 256-bit (quantum) |
Appendix B: Performance comparison
| Operation | ECDSA | BLS | ML-DSA-65 | Slowdown vs ECDSA |
|---|---|---|---|---|
| Key Gen | ~30μs | ~100μs | ~125μs | 4× |
| Sign | ~88μs | ~1,200μs | ~417μs | 5× |
| Verify | ~88μs | ~2,500μs | ~108μs | 1.2× |