LPsLux Proposals
Post-Quantum Cryptography
LP-4400

ML-DSA Post-Quantum Digital Signatures (Family Root)

Final

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

Category
Core
Created
2025-11-22

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) with SignCtxDeterministic as 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

ModeSecurityPublic KeyPrivate KeySignatureSignVerify
ML-DSA-44128-bit (NIST-2)1,312 B2,560 B2,420 B~150μs~80μs
ML-DSA-65192-bit (NIST-3)1,952 B4,032 B3,309 B~417μs~108μs
ML-DSA-87256-bit (NIST-5)2,592 B4,896 B4,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 and related rights waived via CC0.

References

  • LP-4500 — SLH-DSA (hash-based alternative)
  • LP-4600 — ML-KEM (complementary KEM)
  • LP-4940 — Hybrid envelope

Standards

Implementation

  • ~/work/lux/crypto/mldsa/
  • ~/work/lux/evm/precompile/contracts/mldsa/

Appendix A: Size comparison

SchemePublic KeyPrivate KeySignatureSecurity
ECDSA (secp256k1)33 B32 B65 B128-bit (classical)
BLS12-38196 B32 B96 B128-bit (classical)
ML-DSA-441,312 B2,560 B2,420 B128-bit (quantum)
ML-DSA-651,952 B4,032 B3,309 B192-bit (quantum)
ML-DSA-872,592 B4,896 B4,627 B256-bit (quantum)

Appendix B: Performance comparison

OperationECDSABLSML-DSA-65Slowdown vs ECDSA
Key Gen~30μs~100μs~125μs
Sign~88μs~1,200μs~417μs
Verify~88μs~2,500μs~108μs1.2×