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:
Hanzo AI
2026-05-12 10:21:11 -07:00
parent 26cd8bce0d
commit d42b0fbfd3
16 changed files with 151 additions and 311 deletions
+58
View File
@@ -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
View File
@@ -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
+2 -2
View File
@@ -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),
+8 -3
View File
@@ -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++"); }
}
+15
View File
@@ -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 {
+5 -5
View File
@@ -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"
);
}
+18 -23
View File
@@ -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 -3
View File
@@ -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++"); }
}
+13
View File
@@ -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 {
+1 -1
View File
@@ -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),
+9 -4
View File
@@ -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),
}
}
-17
View File
@@ -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
-28
View File
@@ -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.
-38
View File
@@ -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++");
}
}
-104
View File
@@ -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());
}