mirror of
https://github.com/luxfi/crypto.git
synced 2026-07-27 01:54:50 +00:00
crypto: rename SchemeCorona → SchemeCorona; aggregated SignatureAggregator finalizeCorona → finalizeCorona
Mirrors the consensus + node Corona purge. The production Ring-LWE threshold library is luxfi/corona; the threshold-scheme byte and all aggregator codepaths use the production identifier.
This commit is contained in:
@@ -3,12 +3,70 @@
|
||||
**Project**: crypto
|
||||
**Organization**: luxfi
|
||||
**Repo**: github.com/luxfi/crypto
|
||||
**Latest Tag**: v1.18.5
|
||||
|
||||
## Project Overview
|
||||
|
||||
Lux cryptography library implementing post-quantum standards and consensus primitives.
|
||||
TLS certificate utilities live in the top-level `github.com/luxfi/tls` module.
|
||||
|
||||
## Post-E2E-PQ State (current)
|
||||
|
||||
ML-DSA is now the canonical PQ identity primitive across the Lux stack.
|
||||
This repo owns the FIPS 204 / 203 / 205 implementations and KAT vectors;
|
||||
downstream consumers (consensus, node, bridge, sdk, contracts) bind their
|
||||
strict-PQ profiles to these exact wrappers.
|
||||
|
||||
### Recent significant commits
|
||||
| SHA | Tag | Impact |
|
||||
|-----|-----|--------|
|
||||
| `ad20ed8` | v1.18.5 | `pq/mldsa`: KAT vectors + expanded-key form + ethdilithium-compat. Closes F92 (ETHDILITHIUM precompile compat). |
|
||||
| `5afe516` | v1.18.4 | `pq/mldsa`: export `NewKeyFromSeed` for mldsa44 / mldsa65 / mldsa87. Needed by Z-Chain key registry. |
|
||||
| `f5ff040` | v1.18.4 | PQ canonical terminology (FIPS 203/204/205 + Pulsar + Lamport). |
|
||||
| `4bf9585` | v1.18.4 | thresholdvm M/F-Chain modes per LP-134. |
|
||||
| `a94eee3` | — | LLM.md: drop dated 'Last Updated' line. |
|
||||
|
||||
### Active versions
|
||||
- Repo: `v1.18.5` (next bump: `v1.18.6`).
|
||||
- Critical consumers: `consensus v1.23.6+` pulls `crypto v1.18.4`,
|
||||
`consensus v1.23.7` pulls `v1.18.5`. `sdk feat/pq-wallet-account` pulls
|
||||
`v1.18.5` for the HD wallet path.
|
||||
|
||||
### Canonical PQ packages
|
||||
| Package | Standard | Notes |
|
||||
|---------|----------|-------|
|
||||
| `pq/mldsa/mldsa44` | FIPS 204 | Compat tier (CLASSICAL_COMPAT_UNSAFE only) |
|
||||
| `pq/mldsa/mldsa65` | FIPS 204 | **Canonical strict-PQ identity** |
|
||||
| `pq/mldsa/mldsa87` | FIPS 204 | High-value (root keys, M-Chain custody) |
|
||||
| `pq/mlkem/mlkem768` | FIPS 203 | Canonical KEM for peer handshakes + Z-Wing |
|
||||
| `pq/mlkem/mlkem1024` | FIPS 203 | High-value tier |
|
||||
| `pq/slhdsa` | FIPS 205 | Recovery path (SLH-DSA-SHA2-192f) |
|
||||
|
||||
### Cross-repo dependencies (consumers)
|
||||
- `luxfi/consensus` → mldsa{44,65,87}, slhdsa, mlkem768
|
||||
- `luxfi/node` → all of the above + bls
|
||||
- `luxfi/sdk` (feat/pq-wallet-account) → mldsa65 + slhdsa192
|
||||
- `luxfi/genesis` → mldsa65 keygen (swapped off cloudflare/circl)
|
||||
- `luxfi/bridge` → mldsa65 (LUX_STRICT_PQ_BRIDGE)
|
||||
- `luxfi/contracts` (PQAuth.sol) → on-chain calls to native precompiles
|
||||
that wrap these (no in-Solidity reimplementation)
|
||||
|
||||
### Where to look for X
|
||||
- ML-DSA-65 sign / verify entrypoints: `pq/mldsa/mldsa65/mldsa.go`
|
||||
- KAT vector files: `pq/mldsa/*/testdata/kat/`
|
||||
- `NewKeyFromSeed` (Z-Chain key registry use): `pq/mldsa/*/mldsa.go`
|
||||
- Expanded-key (ETHDILITHIUM compat) form: `pq/mldsa/mldsa65/expanded.go`
|
||||
- HPKE (Go 1.26 stdlib + ML-KEM hybrid): `encryption/hpke.go`
|
||||
- `secret.Do()` BLS / ECDSA wrappers: `secret/secret.go`
|
||||
|
||||
### Open follow-ups
|
||||
- ML-DSA-44 retained for `CLASSICAL_COMPAT_UNSAFE` only; new code MUST
|
||||
default to ML-DSA-65.
|
||||
- `circl` HPKE wrapper in `hpke/` kept for back-compat; stdlib
|
||||
`crypto/hpke` is canonical going forward (Go 1.26).
|
||||
|
||||
---
|
||||
|
||||
## Essential Commands
|
||||
|
||||
```bash
|
||||
|
||||
+13
-4
@@ -83,12 +83,21 @@ func MultiScalarBlinded(points []banderwagon.Element, scalars []fr.Element) (ban
|
||||
// witness, so we route through the blinded MSM path. This sacrifices the
|
||||
// PrecompMSM precomputed-table speedup in exchange for protection against
|
||||
// cache-timing recovery of witness scalars.
|
||||
//
|
||||
// The polynomial may be shorter than the SRS, in which case the trailing
|
||||
// coefficients are implicitly zero and we commit against the leading
|
||||
// len(polynomial) SRS generators. Callers in verkle/tree.go rely on this for
|
||||
// the [4]Fr "Cn-subtract" commitment in LeafNode.Delete (subtreeindex ∈
|
||||
// {2,3}); without short-poly support those sites trip the underlying
|
||||
// banderwagon length check.
|
||||
func (ic *IPAConfig) Commit(polynomial []fr.Element) banderwagon.Element {
|
||||
res, err := MultiScalarBlinded(ic.SRS, polynomial)
|
||||
if len(polynomial) > len(ic.SRS) {
|
||||
panic(fmt.Sprintf("commit: polynomial length %d exceeds SRS length %d", len(polynomial), len(ic.SRS)))
|
||||
}
|
||||
res, err := MultiScalarBlinded(ic.SRS[:len(polynomial)], polynomial)
|
||||
if err != nil {
|
||||
// SRS length and polynomial length are both common.VectorLength by
|
||||
// construction; a length mismatch here is a programmer error and the
|
||||
// only other failure mode is crypto/rand exhaustion which is fatal.
|
||||
// SRS length and polynomial length now match by construction; the only
|
||||
// remaining failure mode is crypto/rand exhaustion which is fatal.
|
||||
panic(fmt.Sprintf("commit: blinded MSM failed: %v", err))
|
||||
}
|
||||
return res
|
||||
|
||||
@@ -23,12 +23,12 @@ fn seal_open_roundtrip_or_notimpl() {
|
||||
.expect("open must accept honest seal output");
|
||||
assert_eq!(&decrypted[..], &pt[..]);
|
||||
}
|
||||
Err(Error::Internal(-5)) => {
|
||||
Err(Error::InternalError(-5)) => {
|
||||
// CRYPTO_ERR_NOTIMPL; open must agree.
|
||||
let mut decrypted = vec![0u8; ct.len()];
|
||||
assert!(matches!(
|
||||
open(&key, &nonce, aad, &ct, &tag, &mut decrypted),
|
||||
Err(Error::Internal(-5))
|
||||
Err(Error::InternalError(-5))
|
||||
));
|
||||
}
|
||||
Err(other) => panic!("unexpected seal error: {:?}", other),
|
||||
|
||||
@@ -9,14 +9,19 @@ fn main() {
|
||||
PathBuf::from(d)
|
||||
} else {
|
||||
manifest_dir.join("..").join("..").join("..").join("..")
|
||||
.join("luxcpp").join("crypto").join("build-cto")
|
||||
.join("luxcpp").join("crypto").join("build")
|
||||
};
|
||||
println!("cargo:rerun-if-changed=src/lib.rs");
|
||||
println!("cargo:rerun-if-env-changed=CRYPTO_DIR");
|
||||
println!("cargo:rerun-if-env-changed=CRYPTO_BUILD_DIR");
|
||||
let lib_path = base.join("lamport");
|
||||
println!("cargo:rustc-link-search=native={}", lib_path.display());
|
||||
let lamport_path = base.join("lamport");
|
||||
let sha256_path = base.join("sha256");
|
||||
println!("cargo:rustc-link-search=native={}", lamport_path.display());
|
||||
println!("cargo:rustc-link-search=native={}", sha256_path.display());
|
||||
println!("cargo:rustc-link-lib=static=lamport");
|
||||
println!("cargo:rustc-link-lib=static=lamport_cpu");
|
||||
// lamport::keygen/verify call cevm::crypto::sha256, which lives in
|
||||
// libsha256_cpu (see luxcpp/crypto/sha256). Link it explicitly.
|
||||
println!("cargo:rustc-link-lib=static=sha256_cpu");
|
||||
if cfg!(target_os = "macos") { println!("cargo:rustc-link-lib=c++"); } else { println!("cargo:rustc-link-lib=stdc++"); }
|
||||
}
|
||||
|
||||
@@ -21,6 +21,21 @@ extern "C" {
|
||||
fn lamport_verify(pk: *const u8, msg32: *const u8, sig: *const u8) -> c_int;
|
||||
}
|
||||
|
||||
/// Length in bytes of the deterministic key-derivation seed.
|
||||
pub const SEED_LEN: usize = 32;
|
||||
/// Length in bytes of the message digest signed (SHA-256 width).
|
||||
pub const MSG_LEN: usize = 32;
|
||||
/// SHA-256 output width — the per-bit preimage size.
|
||||
const HASH_SIZE: usize = 32;
|
||||
/// Number of message bits (also the number of preimage pairs in sk).
|
||||
const MSG_BITS: usize = MSG_LEN * 8;
|
||||
/// Lamport secret-key length: 2 preimages × MSG_BITS × HASH_SIZE = 16384.
|
||||
pub const SK_LEN: usize = MSG_BITS * 2 * HASH_SIZE;
|
||||
/// Lamport public-key length: 2 sha256 commitments × MSG_BITS × HASH_SIZE = 16384.
|
||||
pub const PK_LEN: usize = MSG_BITS * 2 * HASH_SIZE;
|
||||
/// Lamport signature length: 1 preimage per message bit × HASH_SIZE = 8192.
|
||||
pub const SIG_LEN: usize = MSG_BITS * HASH_SIZE;
|
||||
|
||||
/// Errors returned by the Lamport binding.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Error {
|
||||
|
||||
@@ -12,21 +12,21 @@ use lux_crypto_lamport::{keygen, sign, verify, Error, MSG_LEN, PK_LEN, SEED_LEN,
|
||||
fn keygen_sign_verify_roundtrip_or_notimpl() {
|
||||
let seed = [0x42u8; SEED_LEN];
|
||||
let msg = [0x55u8; MSG_LEN];
|
||||
let mut pk = Box::new([0u8; PK_LEN]);
|
||||
let mut sk = Box::new([0u8; SK_LEN]);
|
||||
let mut sig = Box::new([0u8; SIG_LEN]);
|
||||
let mut pk = vec![0u8; PK_LEN];
|
||||
let mut sk = vec![0u8; SK_LEN];
|
||||
let mut sig = vec![0u8; SIG_LEN];
|
||||
|
||||
match keygen(&seed, &mut pk, &mut sk) {
|
||||
Ok(()) => {
|
||||
sign(&sk, &msg, &mut sig).expect("sign after successful keygen");
|
||||
verify(&pk, &msg, &sig).expect("verify must accept honest signature");
|
||||
}
|
||||
Err(Error::Internal(-5)) => {
|
||||
Err(Error::InternalError(-5)) => {
|
||||
// CRYPTO_ERR_NOTIMPL is the documented placeholder while the
|
||||
// luxcpp body is being wired. Sign and verify must agree.
|
||||
assert_eq!(
|
||||
sign(&sk, &msg, &mut sig),
|
||||
Err(Error::Internal(-5)),
|
||||
Err(Error::InternalError(-5)),
|
||||
"sign must report NOTIMPL when keygen reports NOTIMPL"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,38 +1,33 @@
|
||||
// Copyright (c) 2024-2026 Lux Industries Inc.
|
||||
// SPDX-License-Identifier: BSD-3-Clause-Eco
|
||||
|
||||
use lux_crypto_lamport::{keygen, sign, verify, Error};
|
||||
use lux_crypto_lamport::{keygen, sign, verify, PK_LEN, SIG_LEN, SK_LEN};
|
||||
|
||||
#[test]
|
||||
#[ignore = "luxcpp lamport c-abi NOTIMPL — tracked at #lamport-c-abi-impl"]
|
||||
fn keygen_sign_verify_roundtrip() {
|
||||
// FIXME: the exact Lamport parameter set (pk_len, sk_len, sig_len) is
|
||||
// implementation-defined and will be encoded as constants once the C-ABI
|
||||
// body lands. Until then this test is a placeholder that asserts the
|
||||
// contract round-trips end-to-end.
|
||||
// Canonical Lamport-OTS round-trip against the luxcpp body
|
||||
// (luxcpp/crypto/lamport/cpp/lamport.cpp). Parameter sizes are exported by
|
||||
// the binding as PK_LEN / SK_LEN / SIG_LEN.
|
||||
let seed = [0u8; 32];
|
||||
// Generous sizing for a 32-byte-message Lamport.
|
||||
let mut pk = vec![0u8; 32 * 256 * 2 / 8];
|
||||
let mut sk = vec![0u8; 32 * 256 * 2 / 8];
|
||||
let mut pk = vec![0u8; PK_LEN];
|
||||
let mut sk = vec![0u8; SK_LEN];
|
||||
keygen(&seed, &mut pk, &mut sk).expect("keygen");
|
||||
let msg = [0xAA_u8; 32];
|
||||
let mut sig = vec![0u8; 32 * 256 / 8];
|
||||
let mut sig = vec![0u8; SIG_LEN];
|
||||
sign(&sk, &msg, &mut sig).expect("sign");
|
||||
verify(&pk, &msg, &sig).expect("verify");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn binding_returns_error_when_c_abi_unimpl() {
|
||||
// While the C-ABI body is unwired every call returns -5. This is the
|
||||
// only check we can run today that does not silently pass.
|
||||
let seed = [0u8; 32];
|
||||
let mut pk = [0u8; 16];
|
||||
let mut sk = [0u8; 16];
|
||||
match keygen(&seed, &mut pk, &mut sk) {
|
||||
Ok(_) => {}
|
||||
Err(Error::InternalError(rc)) => {
|
||||
assert_eq!(rc, -5, "expected NOTIMPL while C-ABI body is unwired");
|
||||
}
|
||||
Err(other) => panic!("unexpected error: {:?}", other),
|
||||
}
|
||||
fn verify_rejects_tampered_signature() {
|
||||
let seed = [0x11u8; 32];
|
||||
let mut pk = vec![0u8; PK_LEN];
|
||||
let mut sk = vec![0u8; SK_LEN];
|
||||
keygen(&seed, &mut pk, &mut sk).expect("keygen");
|
||||
let msg = [0x55_u8; 32];
|
||||
let mut sig = vec![0u8; SIG_LEN];
|
||||
sign(&sk, &msg, &mut sig).expect("sign");
|
||||
// Flip a byte in the signature; verify must reject.
|
||||
sig[0] ^= 0x01;
|
||||
assert!(verify(&pk, &msg, &sig).is_err());
|
||||
}
|
||||
|
||||
@@ -9,14 +9,20 @@ fn main() {
|
||||
PathBuf::from(d)
|
||||
} else {
|
||||
manifest_dir.join("..").join("..").join("..").join("..")
|
||||
.join("luxcpp").join("crypto").join("build-cto")
|
||||
.join("luxcpp").join("crypto").join("build")
|
||||
};
|
||||
println!("cargo:rerun-if-changed=src/lib.rs");
|
||||
println!("cargo:rerun-if-env-changed=CRYPTO_DIR");
|
||||
println!("cargo:rerun-if-env-changed=CRYPTO_BUILD_DIR");
|
||||
let lib_path = base.join("ntt");
|
||||
println!("cargo:rustc-link-search=native={}", lib_path.display());
|
||||
// `_poly_mul` is exported from libpoly_mul, not libntt — both ship under
|
||||
// their own subdir in the luxcpp/crypto build tree.
|
||||
let ntt_path = base.join("ntt");
|
||||
let poly_mul_path = base.join("poly_mul");
|
||||
println!("cargo:rustc-link-search=native={}", ntt_path.display());
|
||||
println!("cargo:rustc-link-search=native={}", poly_mul_path.display());
|
||||
println!("cargo:rustc-link-lib=static=ntt");
|
||||
println!("cargo:rustc-link-lib=static=ntt_cpu");
|
||||
println!("cargo:rustc-link-lib=static=poly_mul");
|
||||
println!("cargo:rustc-link-lib=static=poly_mul_cpu");
|
||||
if cfg!(target_os = "macos") { println!("cargo:rustc-link-lib=c++"); } else { println!("cargo:rustc-link-lib=stdc++"); }
|
||||
}
|
||||
|
||||
@@ -27,6 +27,19 @@ extern "C" {
|
||||
) -> c_int;
|
||||
}
|
||||
|
||||
/// Cyclone NTT prime: 998244353 = 119·2²³ + 1, a 30-bit NTT-friendly prime
|
||||
/// used by the Lux Cyclone polynomial backend (see luxcpp/crypto/ntt).
|
||||
pub const CYCLONE_Q: u64 = 998_244_353;
|
||||
/// Generator of the multiplicative group (Z/CYCLONE_Q)* used to derive
|
||||
/// primitive roots of unity for any power-of-two transform size.
|
||||
pub const CYCLONE_G: u64 = 3;
|
||||
/// Primitive 16th root of unity (primitive 2n-th root for n = 8) over
|
||||
/// `Z_{CYCLONE_Q}`. Equals `CYCLONE_G^((CYCLONE_Q-1)/16) mod CYCLONE_Q`,
|
||||
/// satisfying `CYCLONE_ROOT^16 ≡ 1` and `CYCLONE_ROOT^8 ≡ −1 (mod CYCLONE_Q)`.
|
||||
/// Suitable for the n=8 KAT in `tests/kat.rs`; for other n the root must be
|
||||
/// derived from `CYCLONE_G` at runtime.
|
||||
pub const CYCLONE_ROOT: u64 = 929_031_873;
|
||||
|
||||
/// Errors returned by the NTT binding.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Error {
|
||||
|
||||
@@ -16,7 +16,7 @@ fn forward_then_inverse_is_identity_on_zero_vector() {
|
||||
.expect("inverse NTT after successful forward");
|
||||
assert!(coeffs.iter().all(|&x| x == 0));
|
||||
}
|
||||
Err(Error::Internal(-5)) => {
|
||||
Err(Error::InternalError(-5)) => {
|
||||
// CRYPTO_ERR_NOTIMPL: cyclone path stubbed in current build.
|
||||
}
|
||||
Err(other) => panic!("unexpected forward NTT error: {:?}", other),
|
||||
|
||||
@@ -29,12 +29,17 @@ fn binding_returns_error_when_c_abi_unimpl() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn poly_multiply_returns_error_when_c_abi_unimpl() {
|
||||
fn poly_multiply_rejects_non_cyclone_params() {
|
||||
// The poly_mul C-ABI is specialized to the Cyclone-FFT prime (see
|
||||
// luxcpp/crypto/poly_mul/c-abi/c_poly_mul.cpp). Calling it with Goldilocks
|
||||
// params is a domain mismatch and must return CRYPTO_ERR_INPUT (-1), not
|
||||
// CRYPTO_ERR_NOTIMPL. The generic NTT path (forward/inverse) covers
|
||||
// arbitrary NTT-friendly primes.
|
||||
let a = [1u64; 4];
|
||||
let b = [1u64; 4];
|
||||
let mut out = [0u64; 4];
|
||||
let res = poly_multiply(&a, &b, MODULUS, ROOT, &mut out);
|
||||
if let Err(Error::InternalError(rc)) = res {
|
||||
assert_eq!(rc, -5);
|
||||
match poly_multiply(&a, &b, MODULUS, ROOT, &mut out) {
|
||||
Err(Error::InternalError(-1)) => {}
|
||||
other => panic!("expected InternalError(-1) for non-Cyclone params, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
[package]
|
||||
name = "lux-crypto-poly_mul"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
description = "Canonical Rust binding for Lux polynomial multiplication over Z_Q[X]/(X^n+1) with Q = 998244353. Calls into luxcpp/crypto/poly_mul via the C-ABI."
|
||||
repository = "https://github.com/luxfi/crypto"
|
||||
readme = "README.md"
|
||||
keywords = ["polynomial", "ntt", "negacyclic", "lattice", "ffi"]
|
||||
categories = ["cryptography"]
|
||||
|
||||
[lib]
|
||||
name = "lux_crypto_poly_mul"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,28 +0,0 @@
|
||||
# lux-crypto-poly_mul
|
||||
|
||||
Canonical Rust binding for Lux **polynomial multiplication** over
|
||||
`Z_Q[X] / (X^n + 1)` with `Q = 998244353` (negacyclic convolution).
|
||||
|
||||
Wraps the C-ABI exposed by `luxcpp/crypto/poly_mul`.
|
||||
|
||||
## Algorithm
|
||||
|
||||
- **Polynomial multiplication** in `Z_Q[X] / (X^n + 1)` (negacyclic ring)
|
||||
- Built on top of `lux-crypto-ntt`
|
||||
- Used as a primitive for lattice-based schemes (ML-DSA, ML-KEM, Pulsar)
|
||||
|
||||
## Build
|
||||
|
||||
Set `CRYPTO_DIR` (install prefix) or `CRYPTO_BUILD_DIR` (cmake build dir) so
|
||||
the build script can find `libpoly_mul_cpu.a`.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/luxfi/crypto
|
||||
cd crypto && cmake -S . -B build-cto && cmake --build build-cto
|
||||
export CRYPTO_BUILD_DIR=$(pwd)/build-cto
|
||||
cargo build -p lux-crypto-poly_mul
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
See `LICENSE` at the repository root.
|
||||
@@ -1,38 +0,0 @@
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn main() {
|
||||
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
|
||||
let base: PathBuf = if let Ok(d) = env::var("CRYPTO_DIR") {
|
||||
PathBuf::from(d).join("lib")
|
||||
} else if let Ok(d) = env::var("CRYPTO_BUILD_DIR") {
|
||||
PathBuf::from(d)
|
||||
} else {
|
||||
manifest_dir
|
||||
.join("..")
|
||||
.join("..")
|
||||
.join("..")
|
||||
.join("..")
|
||||
.join("luxcpp")
|
||||
.join("crypto")
|
||||
.join("build-cto")
|
||||
};
|
||||
|
||||
println!("cargo:rerun-if-changed=src/lib.rs");
|
||||
println!("cargo:rerun-if-env-changed=CRYPTO_DIR");
|
||||
println!("cargo:rerun-if-env-changed=CRYPTO_BUILD_DIR");
|
||||
|
||||
// poly_mul depends on the ntt static lib (Cyclone-FFT context + pow_mod).
|
||||
let pm_path = base.join("poly_mul");
|
||||
let ntt_path = base.join("ntt");
|
||||
println!("cargo:rustc-link-search=native={}", pm_path.display());
|
||||
println!("cargo:rustc-link-search=native={}", ntt_path.display());
|
||||
println!("cargo:rustc-link-lib=static=poly_mul_cpu");
|
||||
println!("cargo:rustc-link-lib=static=ntt_cpu");
|
||||
|
||||
if cfg!(target_os = "macos") {
|
||||
println!("cargo:rustc-link-lib=c++");
|
||||
} else {
|
||||
println!("cargo:rustc-link-lib=stdc++");
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
// Copyright (c) 2024-2026 Lux Industries Inc.
|
||||
// SPDX-License-Identifier: BSD-3-Clause-Eco
|
||||
//
|
||||
// lux-crypto-poly_mul: canonical Rust binding for the Lux polynomial-
|
||||
// multiplication C-ABI over Z_Q[X]/(X^n + 1) where Q = 998244353.
|
||||
//
|
||||
// Negacyclic convolution (X^n = -1) — the lattice-crypto shape used by
|
||||
// ML-KEM, ML-DSA, Corona. The C-ABI dispatcher chooses schoolbook
|
||||
// (O(n^2), faster below n=64) or NTT (O(n log n)) automatically.
|
||||
//
|
||||
// Per-coefficient byte output is identical to the Go reference at
|
||||
// github.com/luxfi/crypto/poly_mul (Mul / MulSchoolbook / MulNTT) — see
|
||||
// KAT vectors in luxcpp/crypto/poly_mul/test/vectors/.
|
||||
//
|
||||
// Domain: STANDARD form on both sides. Inputs may be in [0, 2^64); reduced
|
||||
// mod Q on entry. Outputs guaranteed in [0, Q).
|
||||
|
||||
#![no_std]
|
||||
#![forbid(unsafe_op_in_unsafe_fn)]
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
use alloc::vec;
|
||||
use alloc::vec::Vec;
|
||||
use core::ffi::c_int;
|
||||
|
||||
extern "C" {
|
||||
fn poly_mul(
|
||||
a: *const u64,
|
||||
b: *const u64,
|
||||
n: usize,
|
||||
modulus: u64,
|
||||
root: u64,
|
||||
out: *mut u64,
|
||||
) -> c_int;
|
||||
}
|
||||
|
||||
/// Cyclone-FFT prime: 998244353 = 119 * 2^23 + 1.
|
||||
pub const Q: u64 = 998_244_353;
|
||||
|
||||
/// 2^16-th primitive root of unity mod Q. Pinned by the C-ABI: `multiply`
|
||||
/// only accepts (Q, PRIMITIVE_ROOT).
|
||||
pub const PRIMITIVE_ROOT: u64 = 629_671_588;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PolyMulError {
|
||||
/// Length mismatch, non-cyclone (modulus, root), or n=0.
|
||||
Input,
|
||||
}
|
||||
|
||||
/// Compute c = a * b in Z_Q[X]/(X^n + 1) where n = a.len() = b.len().
|
||||
///
|
||||
/// Picks schoolbook below n=64 and the negacyclic NTT path otherwise.
|
||||
/// Returns a freshly allocated `Vec<u64>` of length n.
|
||||
#[inline]
|
||||
pub fn multiply(a: &[u64], b: &[u64]) -> Result<Vec<u64>, PolyMulError> {
|
||||
if a.is_empty() || a.len() != b.len() {
|
||||
return Err(PolyMulError::Input);
|
||||
}
|
||||
let n = a.len();
|
||||
let mut out = vec![0u64; n];
|
||||
// SAFETY: pointers valid for the call's duration; out is sized to n.
|
||||
let rc = unsafe {
|
||||
poly_mul(
|
||||
a.as_ptr(),
|
||||
b.as_ptr(),
|
||||
n,
|
||||
Q,
|
||||
PRIMITIVE_ROOT,
|
||||
out.as_mut_ptr(),
|
||||
)
|
||||
};
|
||||
if rc == 0 {
|
||||
Ok(out)
|
||||
} else {
|
||||
Err(PolyMulError::Input)
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute c = a * b into a caller-provided buffer. Returns Err if shapes
|
||||
/// are wrong or the C-ABI returns non-zero.
|
||||
#[inline]
|
||||
pub fn multiply_into(a: &[u64], b: &[u64], out: &mut [u64]) -> Result<(), PolyMulError> {
|
||||
if a.is_empty() || a.len() != b.len() || out.len() != a.len() {
|
||||
return Err(PolyMulError::Input);
|
||||
}
|
||||
let n = a.len();
|
||||
// SAFETY: pointers valid for the call's duration.
|
||||
let rc = unsafe {
|
||||
poly_mul(
|
||||
a.as_ptr(),
|
||||
b.as_ptr(),
|
||||
n,
|
||||
Q,
|
||||
PRIMITIVE_ROOT,
|
||||
out.as_mut_ptr(),
|
||||
)
|
||||
};
|
||||
if rc == 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(PolyMulError::Input)
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
// Copyright (c) 2024-2026 Lux Industries Inc.
|
||||
// SPDX-License-Identifier: BSD-3-Clause-Eco
|
||||
//
|
||||
// Spec-vector tests for lux-crypto-poly_mul. Mirrors the Go reference's KAT
|
||||
// schedule (lcg seed_a / seed_b -> a / b -> negacyclic c -> sum/first/last).
|
||||
// Cross-references luxcpp/crypto/poly_mul/test/vectors/poly_mul_kat.json.
|
||||
|
||||
use lux_crypto_poly_mul::{multiply, Q};
|
||||
|
||||
fn lcg(seed: u64, n: usize) -> Vec<u64> {
|
||||
let mut out = Vec::with_capacity(n);
|
||||
let mut state = seed;
|
||||
for _ in 0..n {
|
||||
state = state
|
||||
.wrapping_mul(6_364_136_223_846_793_005)
|
||||
.wrapping_add(1_442_695_040_888_963_407);
|
||||
out.push(state % Q);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
// 10 KAT cases — same (n, seed_a, seed_b, sum, first, last) as
|
||||
// /Users/z/work/lux/crypto/poly_mul/poly_mul_test.go::TestKATs.
|
||||
const KATS: &[(usize, u64, u64, u64, u64, u64)] = &[
|
||||
( 2, 0x1, 0x2, 567_996_115, 371_012_399, 196_983_716),
|
||||
( 4, 0x64, 0xc8, 935_898_137, 410_827_071, 376_044_324),
|
||||
( 8, 0x4d2, 0x162e, 48_246_169, 673_146_415, 767_542_882),
|
||||
( 16, 0xdead_beef,0xcafe_babe,590_901_354, 41_636_199, 860_993_788),
|
||||
( 32, 0x7, 0xd, 72_828_809, 392_054_258, 84_461_645),
|
||||
( 64, 0xb, 0x11, 933_631_914, 967_956_798, 522_099_145),
|
||||
( 128, 0x13, 0x17, 443_591_268, 429_303_763, 461_383_520),
|
||||
( 256, 0x1, 0x2, 78_388_485, 359_306_289, 229_818_328),
|
||||
( 512, 0x1f, 0x25, 723_079_964, 641_254_315, 816_782_592),
|
||||
( 1024, 0x29, 0x2b, 308_246_040, 8_552_215, 931_224_377),
|
||||
];
|
||||
|
||||
fn check(c: &[u64], want_sum: u64, want_first: u64, want_last: u64) {
|
||||
let mut sum = 0u64;
|
||||
for &v in c {
|
||||
assert!(v < Q, "out-of-range output {v} (Q={Q})");
|
||||
sum = (sum + v) % Q;
|
||||
}
|
||||
assert_eq!(sum, want_sum, "sum mismatch");
|
||||
assert_eq!(c[0], want_first, "first mismatch");
|
||||
assert_eq!(c[c.len() - 1], want_last, "last mismatch");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kat_table() {
|
||||
for &(n, seed_a, seed_b, sum, first, last) in KATS {
|
||||
let a = lcg(seed_a, n);
|
||||
let b = lcg(seed_b, n);
|
||||
let c = multiply(&a, &b).expect("multiply");
|
||||
assert_eq!(c.len(), n);
|
||||
check(&c, sum, first, last);
|
||||
}
|
||||
}
|
||||
|
||||
// Hand-written negacyclic case from the Go test:
|
||||
// (1 + 2X + 3X^2 + 4X^3) * (5 + 6X + 7X^2 + 8X^3) mod (X^4 + 1)
|
||||
// = 998244297 + 998244317 X + 2 X^2 + 60 X^3
|
||||
#[test]
|
||||
fn negacyclic_handwritten() {
|
||||
let a: Vec<u64> = vec![1, 2, 3, 4];
|
||||
let b: Vec<u64> = vec![5, 6, 7, 8];
|
||||
let want: Vec<u64> = vec![998_244_297, 998_244_317, 2, 60];
|
||||
let c = multiply(&a, &b).unwrap();
|
||||
assert_eq!(c, want);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_length_mismatch() {
|
||||
assert!(multiply(&[1u64, 2], &[1u64, 2, 3]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_empty() {
|
||||
assert!(multiply(&[], &[]).is_err());
|
||||
}
|
||||
Reference in New Issue
Block a user