mirror of
https://github.com/luxfi/precompile.git
synced 2026-07-27 03:33:45 +00:00
feat(precompiles): add ML-KEM key encapsulation precompile
Implements FIPS 203 ML-KEM precompile at address 0x0200...0007: - Encapsulate: generate shared secret + ciphertext from public key - Decapsulate: recover shared secret from ciphertext using private key Security levels: - ML-KEM-512: 128-bit (NIST Level 1) - ML-KEM-768: 192-bit (NIST Level 3) - recommended - ML-KEM-1024: 256-bit (NIST Level 5) Completes the PQ crypto precompile suite alongside ML-DSA and SLH-DSA. See LP-4318 for specification.
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (C) 2025, Lux Industries, Inc. All rights reserved.
|
||||
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
/**
|
||||
* @title IMLKEM
|
||||
* @notice Interface for the ML-KEM (FIPS 203) key encapsulation precompile
|
||||
* @dev Precompile address: 0x0200000000000000000000000000000000000007
|
||||
*
|
||||
* ML-KEM provides post-quantum secure key encapsulation for establishing
|
||||
* shared secrets between parties. This precompile supports three security levels:
|
||||
* - ML-KEM-512: 128-bit security (NIST Level 1)
|
||||
* - ML-KEM-768: 192-bit security (NIST Level 3)
|
||||
* - ML-KEM-1024: 256-bit security (NIST Level 5)
|
||||
*
|
||||
* See LP-4318 for full specification.
|
||||
*/
|
||||
interface IMLKEM {
|
||||
/// @notice ML-KEM mode for 128-bit security (NIST Level 1)
|
||||
uint8 constant MODE_MLKEM_512 = 0x00;
|
||||
|
||||
/// @notice ML-KEM mode for 192-bit security (NIST Level 3)
|
||||
uint8 constant MODE_MLKEM_768 = 0x01;
|
||||
|
||||
/// @notice ML-KEM mode for 256-bit security (NIST Level 5)
|
||||
uint8 constant MODE_MLKEM_1024 = 0x02;
|
||||
|
||||
/// @notice Key sizes for ML-KEM-512
|
||||
uint256 constant MLKEM_512_PUBLIC_KEY_SIZE = 800;
|
||||
uint256 constant MLKEM_512_PRIVATE_KEY_SIZE = 1632;
|
||||
uint256 constant MLKEM_512_CIPHERTEXT_SIZE = 768;
|
||||
|
||||
/// @notice Key sizes for ML-KEM-768
|
||||
uint256 constant MLKEM_768_PUBLIC_KEY_SIZE = 1184;
|
||||
uint256 constant MLKEM_768_PRIVATE_KEY_SIZE = 2400;
|
||||
uint256 constant MLKEM_768_CIPHERTEXT_SIZE = 1088;
|
||||
|
||||
/// @notice Key sizes for ML-KEM-1024
|
||||
uint256 constant MLKEM_1024_PUBLIC_KEY_SIZE = 1568;
|
||||
uint256 constant MLKEM_1024_PRIVATE_KEY_SIZE = 3168;
|
||||
uint256 constant MLKEM_1024_CIPHERTEXT_SIZE = 1568;
|
||||
|
||||
/// @notice Shared secret size (same for all modes)
|
||||
uint256 constant SHARED_SECRET_SIZE = 32;
|
||||
|
||||
/**
|
||||
* @notice Encapsulate a shared secret using a public key
|
||||
* @param mode The ML-KEM mode (0=512, 1=768, 2=1024)
|
||||
* @param publicKey The recipient's public key
|
||||
* @return ciphertext The encapsulated ciphertext to send to recipient
|
||||
* @return sharedSecret The 32-byte shared secret
|
||||
*/
|
||||
function encapsulate(
|
||||
uint8 mode,
|
||||
bytes calldata publicKey
|
||||
) external view returns (
|
||||
bytes memory ciphertext,
|
||||
bytes32 sharedSecret
|
||||
);
|
||||
|
||||
/**
|
||||
* @notice Decapsulate a ciphertext to recover the shared secret
|
||||
* @param mode The ML-KEM mode (0=512, 1=768, 2=1024)
|
||||
* @param privateKey The recipient's private key
|
||||
* @param ciphertext The encapsulated ciphertext from sender
|
||||
* @return sharedSecret The 32-byte shared secret
|
||||
*/
|
||||
function decapsulate(
|
||||
uint8 mode,
|
||||
bytes calldata privateKey,
|
||||
bytes calldata ciphertext
|
||||
) external view returns (bytes32 sharedSecret);
|
||||
}
|
||||
|
||||
/**
|
||||
* @title MLKEMCaller
|
||||
* @notice Helper library for calling the ML-KEM precompile
|
||||
*/
|
||||
library MLKEMCaller {
|
||||
address constant MLKEM_PRECOMPILE = 0x0200000000000000000000000000000000000007;
|
||||
|
||||
uint8 constant OP_ENCAPSULATE = 0x01;
|
||||
uint8 constant OP_DECAPSULATE = 0x02;
|
||||
|
||||
error MLKEMCallFailed();
|
||||
error InvalidResultLength();
|
||||
|
||||
/**
|
||||
* @notice Encapsulate using ML-KEM-768 (recommended)
|
||||
*/
|
||||
function encapsulate768(bytes memory publicKey) internal view returns (bytes memory ciphertext, bytes32 sharedSecret) {
|
||||
return encapsulate(IMLKEM.MODE_MLKEM_768, publicKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Decapsulate using ML-KEM-768 (recommended)
|
||||
*/
|
||||
function decapsulate768(bytes memory privateKey, bytes memory ciphertext) internal view returns (bytes32 sharedSecret) {
|
||||
return decapsulate(IMLKEM.MODE_MLKEM_768, privateKey, ciphertext);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Encapsulate with specified mode
|
||||
*/
|
||||
function encapsulate(uint8 mode, bytes memory publicKey) internal view returns (bytes memory ciphertext, bytes32 sharedSecret) {
|
||||
bytes memory input = abi.encodePacked(OP_ENCAPSULATE, mode, publicKey);
|
||||
|
||||
(bool success, bytes memory result) = MLKEM_PRECOMPILE.staticcall(input);
|
||||
if (!success) revert MLKEMCallFailed();
|
||||
|
||||
// Result is ciphertext || sharedSecret(32 bytes)
|
||||
if (result.length < 32) revert InvalidResultLength();
|
||||
|
||||
uint256 ctLen = result.length - 32;
|
||||
ciphertext = new bytes(ctLen);
|
||||
for (uint256 i = 0; i < ctLen; i++) {
|
||||
ciphertext[i] = result[i];
|
||||
}
|
||||
|
||||
assembly {
|
||||
sharedSecret := mload(add(add(result, 32), ctLen))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Decapsulate with specified mode
|
||||
*/
|
||||
function decapsulate(uint8 mode, bytes memory privateKey, bytes memory ciphertext) internal view returns (bytes32 sharedSecret) {
|
||||
bytes memory input = abi.encodePacked(OP_DECAPSULATE, mode, privateKey, ciphertext);
|
||||
|
||||
(bool success, bytes memory result) = MLKEM_PRECOMPILE.staticcall(input);
|
||||
if (!success) revert MLKEMCallFailed();
|
||||
|
||||
if (result.length != 32) revert InvalidResultLength();
|
||||
|
||||
assembly {
|
||||
sharedSecret := mload(add(result, 32))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
# ML-KEM Precompile
|
||||
|
||||
Post-quantum key encapsulation mechanism precompile implementing FIPS 203 (ML-KEM).
|
||||
|
||||
## Address
|
||||
|
||||
`0x0200000000000000000000000000000000000007`
|
||||
|
||||
## Specification
|
||||
|
||||
See [LP-4318: ML-KEM Post-Quantum Key Encapsulation](https://lps.lux.network/docs/lp-4318-ml-kem-post-quantum-key-encapsulation/)
|
||||
|
||||
## Operations
|
||||
|
||||
### Encapsulate (0x01)
|
||||
|
||||
Generate a shared secret and ciphertext from a public key.
|
||||
|
||||
**Input:**
|
||||
| Offset | Size | Description |
|
||||
|--------|------|-------------|
|
||||
| 0 | 1 | Operation (0x01) |
|
||||
| 1 | 1 | Mode (0x00=512, 0x01=768, 0x02=1024) |
|
||||
| 2 | varies | Public key |
|
||||
|
||||
**Output:**
|
||||
| Offset | Size | Description |
|
||||
|--------|------|-------------|
|
||||
| 0 | varies | Ciphertext |
|
||||
| varies | 32 | Shared secret |
|
||||
|
||||
### Decapsulate (0x02)
|
||||
|
||||
Recover the shared secret from a ciphertext using a private key.
|
||||
|
||||
**Input:**
|
||||
| Offset | Size | Description |
|
||||
|--------|------|-------------|
|
||||
| 0 | 1 | Operation (0x02) |
|
||||
| 1 | 1 | Mode (0x00=512, 0x01=768, 0x02=1024) |
|
||||
| 2 | varies | Private key |
|
||||
| varies | varies | Ciphertext |
|
||||
|
||||
**Output:**
|
||||
| Offset | Size | Description |
|
||||
|--------|------|-------------|
|
||||
| 0 | 32 | Shared secret |
|
||||
|
||||
## Key Sizes
|
||||
|
||||
| Mode | Public Key | Private Key | Ciphertext | Shared Secret |
|
||||
|------|------------|-------------|------------|---------------|
|
||||
| ML-KEM-512 | 800 | 1632 | 768 | 32 |
|
||||
| ML-KEM-768 | 1184 | 2400 | 1088 | 32 |
|
||||
| ML-KEM-1024 | 1568 | 3168 | 1568 | 32 |
|
||||
|
||||
## Gas Costs
|
||||
|
||||
| Operation | ML-KEM-512 | ML-KEM-768 | ML-KEM-1024 |
|
||||
|-----------|------------|------------|-------------|
|
||||
| Encapsulate | 50,000 | 75,000 | 100,000 |
|
||||
| Decapsulate | 60,000 | 90,000 | 120,000 |
|
||||
|
||||
## Security Levels
|
||||
|
||||
- **ML-KEM-512**: 128-bit security (NIST Level 1)
|
||||
- **ML-KEM-768**: 192-bit security (NIST Level 3) - **Recommended**
|
||||
- **ML-KEM-1024**: 256-bit security (NIST Level 5)
|
||||
|
||||
## Usage Example (Solidity)
|
||||
|
||||
```solidity
|
||||
import {MLKEMCaller} from "./IMLKEM.sol";
|
||||
|
||||
contract QuantumSecureExchange {
|
||||
using MLKEMCaller for *;
|
||||
|
||||
function establishSecret(bytes calldata recipientPubKey)
|
||||
external view
|
||||
returns (bytes memory ciphertext, bytes32 sharedSecret)
|
||||
{
|
||||
return MLKEMCaller.encapsulate768(recipientPubKey);
|
||||
}
|
||||
|
||||
function recoverSecret(
|
||||
bytes calldata privateKey,
|
||||
bytes calldata ciphertext
|
||||
) external view returns (bytes32 sharedSecret) {
|
||||
return MLKEMCaller.decapsulate768(privateKey, ciphertext);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [FIPS 203: Module-Lattice-Based Key-Encapsulation Mechanism Standard](https://csrc.nist.gov/pubs/fips/203/final)
|
||||
- [NIST Post-Quantum Cryptography](https://csrc.nist.gov/projects/post-quantum-cryptography)
|
||||
@@ -0,0 +1,257 @@
|
||||
// Copyright (C) 2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// Package mlkem implements the ML-KEM (FIPS 203) key encapsulation precompile.
|
||||
// Address: 0x0200000000000000000000000000000000000007
|
||||
//
|
||||
// See LP-4318 for full specification.
|
||||
package mlkem
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/luxfi/crypto/mlkem"
|
||||
"github.com/luxfi/geth/common"
|
||||
"github.com/luxfi/precompiles/contract"
|
||||
)
|
||||
|
||||
var (
|
||||
// ContractAddress is the address of the ML-KEM precompile
|
||||
ContractAddress = common.HexToAddress("0x0200000000000000000000000000000000000007")
|
||||
|
||||
// Singleton instance
|
||||
MLKEMPrecompile = &mlkemPrecompile{}
|
||||
|
||||
_ contract.StatefulPrecompiledContract = &mlkemPrecompile{}
|
||||
|
||||
ErrInvalidInputLength = errors.New("invalid input length")
|
||||
ErrInvalidMode = errors.New("invalid ML-KEM mode")
|
||||
ErrUnsupportedMode = errors.New("unsupported ML-KEM mode")
|
||||
ErrUnsupportedOperation = errors.New("unsupported operation")
|
||||
ErrEncapsulationFailed = errors.New("encapsulation failed")
|
||||
ErrDecapsulationFailed = errors.New("decapsulation failed")
|
||||
)
|
||||
|
||||
// Operation selectors
|
||||
const (
|
||||
OpEncapsulate = 0x01 // Generate shared secret + ciphertext from public key
|
||||
OpDecapsulate = 0x02 // Recover shared secret from ciphertext using private key
|
||||
)
|
||||
|
||||
// ML-KEM modes (FIPS 203)
|
||||
const (
|
||||
ModeMLKEM512 uint8 = 0x00 // ML-KEM-512 (128-bit security, NIST Level 1)
|
||||
ModeMLKEM768 uint8 = 0x01 // ML-KEM-768 (192-bit security, NIST Level 3)
|
||||
ModeMLKEM1024 uint8 = 0x02 // ML-KEM-1024 (256-bit security, NIST Level 5)
|
||||
)
|
||||
|
||||
// Size constants for ML-KEM-512 (NIST Level 1)
|
||||
const (
|
||||
MLKEM512PublicKeySize = 800
|
||||
MLKEM512PrivateKeySize = 1632
|
||||
MLKEM512CiphertextSize = 768
|
||||
MLKEM512SharedKeySize = 32
|
||||
)
|
||||
|
||||
// Size constants for ML-KEM-768 (NIST Level 3)
|
||||
const (
|
||||
MLKEM768PublicKeySize = 1184
|
||||
MLKEM768PrivateKeySize = 2400
|
||||
MLKEM768CiphertextSize = 1088
|
||||
MLKEM768SharedKeySize = 32
|
||||
)
|
||||
|
||||
// Size constants for ML-KEM-1024 (NIST Level 5)
|
||||
const (
|
||||
MLKEM1024PublicKeySize = 1568
|
||||
MLKEM1024PrivateKeySize = 3168
|
||||
MLKEM1024CiphertextSize = 1568
|
||||
MLKEM1024SharedKeySize = 32
|
||||
)
|
||||
|
||||
// Gas costs - based on computational complexity
|
||||
const (
|
||||
// Encapsulation gas costs per mode
|
||||
MLKEM512EncapsulateGas uint64 = 50_000 // Smaller, faster
|
||||
MLKEM768EncapsulateGas uint64 = 75_000 // Medium
|
||||
MLKEM1024EncapsulateGas uint64 = 100_000 // Larger, slower
|
||||
|
||||
// Decapsulation gas costs per mode
|
||||
MLKEM512DecapsulateGas uint64 = 60_000 // Slightly more than encaps
|
||||
MLKEM768DecapsulateGas uint64 = 90_000
|
||||
MLKEM1024DecapsulateGas uint64 = 120_000
|
||||
)
|
||||
|
||||
type mlkemPrecompile struct{}
|
||||
|
||||
// Address returns the address of the ML-KEM precompile
|
||||
func (p *mlkemPrecompile) Address() common.Address {
|
||||
return ContractAddress
|
||||
}
|
||||
|
||||
// getModeParams returns the parameters for a given ML-KEM mode
|
||||
func getModeParams(mode uint8) (pubKeySize, privKeySize, ctSize, sharedSize int, encapsGas, decapsGas uint64, mlkemMode mlkem.Mode, err error) {
|
||||
switch mode {
|
||||
case ModeMLKEM512:
|
||||
return MLKEM512PublicKeySize, MLKEM512PrivateKeySize, MLKEM512CiphertextSize, MLKEM512SharedKeySize,
|
||||
MLKEM512EncapsulateGas, MLKEM512DecapsulateGas, mlkem.MLKEM512, nil
|
||||
case ModeMLKEM768:
|
||||
return MLKEM768PublicKeySize, MLKEM768PrivateKeySize, MLKEM768CiphertextSize, MLKEM768SharedKeySize,
|
||||
MLKEM768EncapsulateGas, MLKEM768DecapsulateGas, mlkem.MLKEM768, nil
|
||||
case ModeMLKEM1024:
|
||||
return MLKEM1024PublicKeySize, MLKEM1024PrivateKeySize, MLKEM1024CiphertextSize, MLKEM1024SharedKeySize,
|
||||
MLKEM1024EncapsulateGas, MLKEM1024DecapsulateGas, mlkem.MLKEM1024, nil
|
||||
default:
|
||||
return 0, 0, 0, 0, 0, 0, 0, ErrUnsupportedMode
|
||||
}
|
||||
}
|
||||
|
||||
// RequiredGas calculates the gas required for ML-KEM operations
|
||||
func (p *mlkemPrecompile) RequiredGas(input []byte) uint64 {
|
||||
if len(input) < 2 {
|
||||
return MLKEM768EncapsulateGas // Default for invalid input
|
||||
}
|
||||
|
||||
op := input[0]
|
||||
mode := input[1]
|
||||
|
||||
_, _, _, _, encapsGas, decapsGas, _, err := getModeParams(mode)
|
||||
if err != nil {
|
||||
return MLKEM768EncapsulateGas // Default for invalid mode
|
||||
}
|
||||
|
||||
switch op {
|
||||
case OpEncapsulate:
|
||||
return encapsGas
|
||||
case OpDecapsulate:
|
||||
return decapsGas
|
||||
default:
|
||||
return MLKEM768EncapsulateGas
|
||||
}
|
||||
}
|
||||
|
||||
// Run implements the ML-KEM precompile
|
||||
// Input format:
|
||||
// [0] = operation byte (0x01 = encapsulate, 0x02 = decapsulate)
|
||||
// [1] = mode byte (0x00 = 512, 0x01 = 768, 0x02 = 1024)
|
||||
// [2:...] = operation-specific data
|
||||
//
|
||||
// Encapsulate input:
|
||||
// [2:2+pubKeySize] = public key
|
||||
//
|
||||
// Encapsulate output:
|
||||
// [0:ctSize] = ciphertext
|
||||
// [ctSize:ctSize+32] = shared secret (32 bytes)
|
||||
//
|
||||
// Decapsulate input:
|
||||
// [2:2+privKeySize] = private key
|
||||
// [2+privKeySize:2+privKeySize+ctSize] = ciphertext
|
||||
//
|
||||
// Decapsulate output:
|
||||
// [0:32] = shared secret (32 bytes)
|
||||
func (p *mlkemPrecompile) Run(
|
||||
accessibleState contract.AccessibleState,
|
||||
caller common.Address,
|
||||
addr common.Address,
|
||||
input []byte,
|
||||
suppliedGas uint64,
|
||||
readOnly bool,
|
||||
) ([]byte, uint64, error) {
|
||||
// Calculate required gas
|
||||
gasCost := p.RequiredGas(input)
|
||||
if suppliedGas < gasCost {
|
||||
return nil, 0, errors.New("out of gas")
|
||||
}
|
||||
|
||||
// Minimum: op byte + mode byte
|
||||
if len(input) < 2 {
|
||||
return nil, suppliedGas - gasCost, ErrInvalidInputLength
|
||||
}
|
||||
|
||||
op := input[0]
|
||||
mode := input[1]
|
||||
|
||||
var result []byte
|
||||
var err error
|
||||
|
||||
switch op {
|
||||
case OpEncapsulate:
|
||||
result, err = p.encapsulate(mode, input[2:])
|
||||
case OpDecapsulate:
|
||||
result, err = p.decapsulate(mode, input[2:])
|
||||
default:
|
||||
err = fmt.Errorf("%w: 0x%02x", ErrUnsupportedOperation, op)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, suppliedGas - gasCost, err
|
||||
}
|
||||
|
||||
return result, suppliedGas - gasCost, nil
|
||||
}
|
||||
|
||||
// encapsulate generates a shared secret and ciphertext from a public key
|
||||
func (p *mlkemPrecompile) encapsulate(mode uint8, input []byte) ([]byte, error) {
|
||||
pubKeySize, _, ctSize, sharedSize, _, _, mlkemMode, err := getModeParams(mode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Validate input length
|
||||
if len(input) != pubKeySize {
|
||||
return nil, fmt.Errorf("%w: expected %d bytes for public key, got %d",
|
||||
ErrInvalidInputLength, pubKeySize, len(input))
|
||||
}
|
||||
|
||||
// Parse public key
|
||||
pk, err := mlkem.PublicKeyFromBytes(input, mlkemMode)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid public key: %w", err)
|
||||
}
|
||||
|
||||
// Encapsulate - generates ciphertext and shared secret
|
||||
ciphertext, sharedSecret, err := pk.Encapsulate()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrEncapsulationFailed, err)
|
||||
}
|
||||
|
||||
// Return ciphertext || sharedSecret
|
||||
result := make([]byte, ctSize+sharedSize)
|
||||
copy(result[:ctSize], ciphertext)
|
||||
copy(result[ctSize:], sharedSecret)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// decapsulate recovers the shared secret from a ciphertext using a private key
|
||||
func (p *mlkemPrecompile) decapsulate(mode uint8, input []byte) ([]byte, error) {
|
||||
_, privKeySize, ctSize, _, _, _, mlkemMode, err := getModeParams(mode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Validate input length
|
||||
expectedLen := privKeySize + ctSize
|
||||
if len(input) != expectedLen {
|
||||
return nil, fmt.Errorf("%w: expected %d bytes (privKey=%d + ct=%d), got %d",
|
||||
ErrInvalidInputLength, expectedLen, privKeySize, ctSize, len(input))
|
||||
}
|
||||
|
||||
// Parse private key
|
||||
privKeyBytes := input[:privKeySize]
|
||||
ciphertext := input[privKeySize:]
|
||||
|
||||
sk, err := mlkem.PrivateKeyFromBytes(privKeyBytes, mlkemMode)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid private key: %w", err)
|
||||
}
|
||||
|
||||
// Decapsulate - recovers shared secret
|
||||
sharedSecret, err := sk.Decapsulate(ciphertext)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrDecapsulationFailed, err)
|
||||
}
|
||||
|
||||
return sharedSecret, nil
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
// Copyright (C) 2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package mlkem
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/crypto/mlkem"
|
||||
"github.com/luxfi/geth/common"
|
||||
)
|
||||
|
||||
// mockAccessibleState implements contract.AccessibleState for testing
|
||||
type mockAccessibleState struct{}
|
||||
|
||||
func (m *mockAccessibleState) GetStateDB() interface{} { return nil }
|
||||
func (m *mockAccessibleState) GetBlockContext() interface{} { return nil }
|
||||
|
||||
func TestMLKEMPrecompileAddress(t *testing.T) {
|
||||
expected := common.HexToAddress("0x0200000000000000000000000000000000000007")
|
||||
if MLKEMPrecompile.Address() != expected {
|
||||
t.Errorf("expected address %s, got %s", expected.Hex(), MLKEMPrecompile.Address().Hex())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequiredGas(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []byte
|
||||
expected uint64
|
||||
}{
|
||||
{"empty input", []byte{}, MLKEM768EncapsulateGas},
|
||||
{"only op byte", []byte{OpEncapsulate}, MLKEM768EncapsulateGas},
|
||||
{"encapsulate 512", []byte{OpEncapsulate, ModeMLKEM512}, MLKEM512EncapsulateGas},
|
||||
{"encapsulate 768", []byte{OpEncapsulate, ModeMLKEM768}, MLKEM768EncapsulateGas},
|
||||
{"encapsulate 1024", []byte{OpEncapsulate, ModeMLKEM1024}, MLKEM1024EncapsulateGas},
|
||||
{"decapsulate 512", []byte{OpDecapsulate, ModeMLKEM512}, MLKEM512DecapsulateGas},
|
||||
{"decapsulate 768", []byte{OpDecapsulate, ModeMLKEM768}, MLKEM768DecapsulateGas},
|
||||
{"decapsulate 1024", []byte{OpDecapsulate, ModeMLKEM1024}, MLKEM1024DecapsulateGas},
|
||||
{"invalid mode", []byte{OpEncapsulate, 0xFF}, MLKEM768EncapsulateGas},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gas := MLKEMPrecompile.RequiredGas(tt.input)
|
||||
if gas != tt.expected {
|
||||
t.Errorf("expected gas %d, got %d", tt.expected, gas)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncapsulateDecapsulate(t *testing.T) {
|
||||
modes := []struct {
|
||||
name string
|
||||
mode uint8
|
||||
mlkemMode mlkem.Mode
|
||||
}{
|
||||
{"ML-KEM-512", ModeMLKEM512, mlkem.MLKEM512},
|
||||
{"ML-KEM-768", ModeMLKEM768, mlkem.MLKEM768},
|
||||
{"ML-KEM-1024", ModeMLKEM1024, mlkem.MLKEM1024},
|
||||
}
|
||||
|
||||
for _, m := range modes {
|
||||
t.Run(m.name, func(t *testing.T) {
|
||||
// Generate key pair
|
||||
pk, sk, err := mlkem.GenerateKey(m.mlkemMode)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generate key pair: %v", err)
|
||||
}
|
||||
|
||||
// Build encapsulate input
|
||||
encInput := make([]byte, 2+len(pk.Bytes()))
|
||||
encInput[0] = OpEncapsulate
|
||||
encInput[1] = m.mode
|
||||
copy(encInput[2:], pk.Bytes())
|
||||
|
||||
// Run encapsulate
|
||||
result, remainingGas, err := MLKEMPrecompile.Run(
|
||||
nil, // accessibleState not used
|
||||
common.Address{},
|
||||
ContractAddress,
|
||||
encInput,
|
||||
1_000_000, // suppliedGas
|
||||
false, // readOnly
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("encapsulate failed: %v", err)
|
||||
}
|
||||
if remainingGas == 0 {
|
||||
t.Error("expected remaining gas > 0")
|
||||
}
|
||||
|
||||
// Parse result: ciphertext || sharedSecret
|
||||
ctSize := mlkem.GetCiphertextSize(m.mlkemMode)
|
||||
if len(result) != ctSize+32 {
|
||||
t.Fatalf("expected result length %d, got %d", ctSize+32, len(result))
|
||||
}
|
||||
|
||||
ciphertext := result[:ctSize]
|
||||
sharedSecret1 := result[ctSize:]
|
||||
|
||||
// Build decapsulate input
|
||||
decInput := make([]byte, 2+len(sk.Bytes())+len(ciphertext))
|
||||
decInput[0] = OpDecapsulate
|
||||
decInput[1] = m.mode
|
||||
copy(decInput[2:], sk.Bytes())
|
||||
copy(decInput[2+len(sk.Bytes()):], ciphertext)
|
||||
|
||||
// Run decapsulate
|
||||
sharedSecret2, remainingGas, err := MLKEMPrecompile.Run(
|
||||
nil,
|
||||
common.Address{},
|
||||
ContractAddress,
|
||||
decInput,
|
||||
1_000_000,
|
||||
false,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("decapsulate failed: %v", err)
|
||||
}
|
||||
if remainingGas == 0 {
|
||||
t.Error("expected remaining gas > 0")
|
||||
}
|
||||
|
||||
// Verify shared secrets match
|
||||
if !bytes.Equal(sharedSecret1, sharedSecret2) {
|
||||
t.Error("shared secrets do not match")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidInputs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []byte
|
||||
}{
|
||||
{"empty", []byte{}},
|
||||
{"only op", []byte{OpEncapsulate}},
|
||||
{"invalid op", []byte{0xFF, ModeMLKEM768}},
|
||||
{"encapsulate no key", []byte{OpEncapsulate, ModeMLKEM768}},
|
||||
{"encapsulate wrong size", []byte{OpEncapsulate, ModeMLKEM768, 0x01, 0x02, 0x03}},
|
||||
{"decapsulate no data", []byte{OpDecapsulate, ModeMLKEM768}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, _, err := MLKEMPrecompile.Run(
|
||||
nil,
|
||||
common.Address{},
|
||||
ContractAddress,
|
||||
tt.input,
|
||||
1_000_000,
|
||||
false,
|
||||
)
|
||||
if err == nil {
|
||||
t.Error("expected error for invalid input")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOutOfGas(t *testing.T) {
|
||||
// Generate a valid key for testing
|
||||
pk, _, err := mlkem.GenerateKey(mlkem.MLKEM768)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generate key: %v", err)
|
||||
}
|
||||
|
||||
input := make([]byte, 2+len(pk.Bytes()))
|
||||
input[0] = OpEncapsulate
|
||||
input[1] = ModeMLKEM768
|
||||
copy(input[2:], pk.Bytes())
|
||||
|
||||
// Run with insufficient gas
|
||||
_, _, err = MLKEMPrecompile.Run(
|
||||
nil,
|
||||
common.Address{},
|
||||
ContractAddress,
|
||||
input,
|
||||
100, // Very low gas
|
||||
false,
|
||||
)
|
||||
if err == nil || err.Error() != "out of gas" {
|
||||
t.Errorf("expected 'out of gas' error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkEncapsulate(b *testing.B) {
|
||||
modes := []struct {
|
||||
name string
|
||||
mode uint8
|
||||
mlkemMode mlkem.Mode
|
||||
}{
|
||||
{"ML-KEM-512", ModeMLKEM512, mlkem.MLKEM512},
|
||||
{"ML-KEM-768", ModeMLKEM768, mlkem.MLKEM768},
|
||||
{"ML-KEM-1024", ModeMLKEM1024, mlkem.MLKEM1024},
|
||||
}
|
||||
|
||||
for _, m := range modes {
|
||||
pk, _, _ := mlkem.GenerateKey(m.mlkemMode)
|
||||
input := make([]byte, 2+len(pk.Bytes()))
|
||||
input[0] = OpEncapsulate
|
||||
input[1] = m.mode
|
||||
copy(input[2:], pk.Bytes())
|
||||
|
||||
b.Run(m.name, func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
MLKEMPrecompile.Run(nil, common.Address{}, ContractAddress, input, 1_000_000, false)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkDecapsulate(b *testing.B) {
|
||||
modes := []struct {
|
||||
name string
|
||||
mode uint8
|
||||
mlkemMode mlkem.Mode
|
||||
}{
|
||||
{"ML-KEM-512", ModeMLKEM512, mlkem.MLKEM512},
|
||||
{"ML-KEM-768", ModeMLKEM768, mlkem.MLKEM768},
|
||||
{"ML-KEM-1024", ModeMLKEM1024, mlkem.MLKEM1024},
|
||||
}
|
||||
|
||||
for _, m := range modes {
|
||||
pk, sk, _ := mlkem.GenerateKey(m.mlkemMode)
|
||||
ct, _, _ := pk.Encapsulate()
|
||||
|
||||
input := make([]byte, 2+len(sk.Bytes())+len(ct))
|
||||
input[0] = OpDecapsulate
|
||||
input[1] = m.mode
|
||||
copy(input[2:], sk.Bytes())
|
||||
copy(input[2+len(sk.Bytes()):], ct)
|
||||
|
||||
b.Run(m.name, func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
MLKEMPrecompile.Run(nil, common.Address{}, ContractAddress, input, 1_000_000, false)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package mlkem
|
||||
|
||||
import (
|
||||
"github.com/luxfi/geth/common"
|
||||
"github.com/luxfi/precompiles/contract"
|
||||
)
|
||||
|
||||
var (
|
||||
// Module is the precompile module singleton
|
||||
Module = &module{
|
||||
address: ContractAddress,
|
||||
contract: MLKEMPrecompile,
|
||||
}
|
||||
)
|
||||
|
||||
type module struct {
|
||||
address common.Address
|
||||
contract contract.StatefulPrecompiledContract
|
||||
}
|
||||
|
||||
// Address returns the address where the stateful precompile is accessible.
|
||||
func (m *module) Address() common.Address {
|
||||
return m.address
|
||||
}
|
||||
|
||||
// Contract returns a thread-safe singleton that can be used as the StatefulPrecompiledContract
|
||||
func (m *module) Contract() contract.StatefulPrecompiledContract {
|
||||
return m.contract
|
||||
}
|
||||
|
||||
// Configure is a no-op for ML-KEM as it has no configuration
|
||||
func (m *module) Configure(
|
||||
_ contract.StateDB,
|
||||
_ common.Address,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user