PQ Precompiles

Key Encapsulation

ML-KEM (FIPS 203) post-quantum key encapsulation precompile for the Lux EVM.

Overview

ML-KEM (Module-Lattice-Based Key Encapsulation Mechanism), standardized as FIPS 203 in August 2024, provides post-quantum secure key exchange. The Lux EVM includes a precompile for on-chain ML-KEM operations, enabling smart contracts to participate in quantum-resistant key agreement protocols.

ML-KEM Config

PropertyValue
Address0x0200000000000000000000000000000000000007
OperationsEncapsulate, Decapsulate (view)
Supported LevelsML-KEM-512, ML-KEM-768, ML-KEM-1024
Gas Cost8,000 (encapsulate), 10,000 (decapsulate)

Unlike the signature verification precompiles which simply return valid/invalid, ML-KEM is a key encapsulation mechanism: it produces a shared secret and a ciphertext. The precompile supports both the encapsulation and decapsulation operations.

Parameter Sets

Parameter SetSecurity LevelPublic KeyCiphertextShared Secret
ML-KEM-512NIST Level 1 (128-bit)800 bytes768 bytes32 bytes
ML-KEM-768NIST Level 3 (192-bit)1,184 bytes1,088 bytes32 bytes
ML-KEM-1024NIST Level 5 (256-bit)1,568 bytes1,568 bytes32 bytes

Operations

Encapsulate

Given a public key, produce a shared secret and ciphertext. The ciphertext can be sent to the key holder, who decapsulates it to recover the same shared secret.

Input format: 0x01 || public_key

Output: shared_secret (32 bytes) || ciphertext

import { createPublicClient, http, encodePacked } from 'viem'
import { lux } from 'viem/chains'

const client = createPublicClient({
  chain: lux,
  transport: http(),
})

const ML_KEM_ADDRESS = '0x0200000000000000000000000000000000000007'

// Encapsulate: produce shared secret + ciphertext
const result = await client.call({
  to: ML_KEM_ADDRESS,
  data: encodePacked(
    ['uint8', 'bytes'],
    [0x01, recipientPublicKey]  // 0x01 = encapsulate operation
  ),
})

// result.data contains: shared_secret (32 bytes) || ciphertext
const sharedSecret = result.data.slice(0, 66)  // 0x + 64 hex chars = 32 bytes
const ciphertext = result.data.slice(66)

Decapsulate

Given a secret key and ciphertext, recover the shared secret.

Input format: 0x02 || secret_key || ciphertext

Output: shared_secret (32 bytes)

Decapsulation requires the secret key, which should never be stored on-chain. The decapsulate operation is intended for use in off-chain computation (e.g., via eth_call) or within privacy-preserving enclaves. Submitting a secret key in an on-chain transaction exposes it publicly.

Use Cases

Encrypted On-Chain Messaging

Smart contracts can establish shared secrets between parties for encrypted communication channels, with the key exchange itself being quantum-resistant:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract QuantumSafeChannel {
    address constant ML_KEM = 0x0200000000000000000000000000000000000007;

    // Recipient registers their ML-KEM public key
    mapping(address => bytes) public publicKeys;

    event CiphertextPublished(address indexed sender, address indexed recipient, bytes ciphertext);

    function registerPublicKey(bytes calldata mlKemPublicKey) external {
        publicKeys[msg.sender] = mlKemPublicKey;
    }

    function initiateChannel(address recipient) external returns (bytes memory ciphertext) {
        bytes memory recipientKey = publicKeys[recipient];
        require(recipientKey.length > 0, "Recipient not registered");

        // Encapsulate to produce shared secret + ciphertext
        (bool success, bytes memory result) = ML_KEM.staticcall(
            abi.encodePacked(uint8(0x01), recipientKey)
        );
        require(success, "Encapsulation failed");

        // Shared secret (first 32 bytes) stays in contract state or is used ephemerally
        // Ciphertext is published for the recipient to decapsulate off-chain
        ciphertext = new bytes(result.length - 32);
        for (uint i = 0; i < ciphertext.length; i++) {
            ciphertext[i] = result[i + 32];
        }

        emit CiphertextPublished(msg.sender, recipient, ciphertext);
    }
}

Hybrid Key Exchange

Combine ML-KEM with classical ECDH for defense-in-depth. The final shared secret is derived from both key exchanges, so an attacker must break both to compromise the channel:

import { keccak256, encodePacked } from 'viem'

// Classical ECDH shared secret (from secp256k1)
const classicalSecret = deriveECDHSecret(myPrivateKey, theirPublicKey)

// Post-quantum ML-KEM shared secret (from precompile)
const pqResult = await client.call({
  to: ML_KEM_ADDRESS,
  data: encodePacked(['uint8', 'bytes'], [0x01, theirMLKEMPublicKey]),
})
const pqSecret = pqResult.data.slice(0, 66)

// Hybrid shared secret: hash both together
const hybridSecret = keccak256(
  encodePacked(['bytes', 'bytes'], [classicalSecret, pqSecret])
)

Future-Proof Token Bridges

Bridge protocols can use ML-KEM to establish quantum-resistant encrypted channels between validator nodes, ensuring that bridge messages remain confidential even if captured traffic is later decrypted by a quantum computer ("harvest now, decrypt later" attacks).

Is this guide helpful?