From d42b0fbfd3cfb46748fa15102ccf0aaae72b4cf4 Mon Sep 17 00:00:00 2001 From: Hanzo AI Date: Tue, 12 May 2026 10:21:11 -0700 Subject: [PATCH] =?UTF-8?q?crypto:=20rename=20SchemeCorona=20=E2=86=92=20S?= =?UTF-8?q?chemeCorona;=20aggregated=20SignatureAggregator=20finalizeCoron?= =?UTF-8?q?a=20=E2=86=92=20finalizeCorona?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- LLM.md | 58 ++++++++++ ipa/ipa/config.go | 17 ++- rust/lux-crypto-aead/tests/kat.rs | 4 +- rust/lux-crypto-lamport/build.rs | 11 +- rust/lux-crypto-lamport/src/lib.rs | 15 +++ rust/lux-crypto-lamport/tests/kat.rs | 10 +- rust/lux-crypto-lamport/tests/spec_vectors.rs | 41 +++---- rust/lux-crypto-ntt/build.rs | 12 +- rust/lux-crypto-ntt/src/lib.rs | 13 +++ rust/lux-crypto-ntt/tests/kat.rs | 2 +- rust/lux-crypto-ntt/tests/spec_vectors.rs | 13 ++- rust/lux-crypto-poly_mul/Cargo.toml | 17 --- rust/lux-crypto-poly_mul/README.md | 28 ----- rust/lux-crypto-poly_mul/build.rs | 38 ------- rust/lux-crypto-poly_mul/src/lib.rs | 104 ------------------ .../lux-crypto-poly_mul/tests/spec_vectors.rs | 79 ------------- 16 files changed, 151 insertions(+), 311 deletions(-) delete mode 100644 rust/lux-crypto-poly_mul/Cargo.toml delete mode 100644 rust/lux-crypto-poly_mul/README.md delete mode 100644 rust/lux-crypto-poly_mul/build.rs delete mode 100644 rust/lux-crypto-poly_mul/src/lib.rs delete mode 100644 rust/lux-crypto-poly_mul/tests/spec_vectors.rs diff --git a/LLM.md b/LLM.md index 17ce9e0..1fb361f 100644 --- a/LLM.md +++ b/LLM.md @@ -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 diff --git a/ipa/ipa/config.go b/ipa/ipa/config.go index d14be1f..0d9779b 100644 --- a/ipa/ipa/config.go +++ b/ipa/ipa/config.go @@ -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 diff --git a/rust/lux-crypto-aead/tests/kat.rs b/rust/lux-crypto-aead/tests/kat.rs index abffecb..1921095 100644 --- a/rust/lux-crypto-aead/tests/kat.rs +++ b/rust/lux-crypto-aead/tests/kat.rs @@ -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), diff --git a/rust/lux-crypto-lamport/build.rs b/rust/lux-crypto-lamport/build.rs index 1ee191a..cb7062a 100644 --- a/rust/lux-crypto-lamport/build.rs +++ b/rust/lux-crypto-lamport/build.rs @@ -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++"); } } diff --git a/rust/lux-crypto-lamport/src/lib.rs b/rust/lux-crypto-lamport/src/lib.rs index 0dda550..5e0edd2 100644 --- a/rust/lux-crypto-lamport/src/lib.rs +++ b/rust/lux-crypto-lamport/src/lib.rs @@ -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 { diff --git a/rust/lux-crypto-lamport/tests/kat.rs b/rust/lux-crypto-lamport/tests/kat.rs index 60c58d0..f1f7a78 100644 --- a/rust/lux-crypto-lamport/tests/kat.rs +++ b/rust/lux-crypto-lamport/tests/kat.rs @@ -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" ); } diff --git a/rust/lux-crypto-lamport/tests/spec_vectors.rs b/rust/lux-crypto-lamport/tests/spec_vectors.rs index 249db67..ed18c1d 100644 --- a/rust/lux-crypto-lamport/tests/spec_vectors.rs +++ b/rust/lux-crypto-lamport/tests/spec_vectors.rs @@ -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()); } diff --git a/rust/lux-crypto-ntt/build.rs b/rust/lux-crypto-ntt/build.rs index babab39..c36ecf8 100644 --- a/rust/lux-crypto-ntt/build.rs +++ b/rust/lux-crypto-ntt/build.rs @@ -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++"); } } diff --git a/rust/lux-crypto-ntt/src/lib.rs b/rust/lux-crypto-ntt/src/lib.rs index 8e0ae1b..22f9b2e 100644 --- a/rust/lux-crypto-ntt/src/lib.rs +++ b/rust/lux-crypto-ntt/src/lib.rs @@ -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 { diff --git a/rust/lux-crypto-ntt/tests/kat.rs b/rust/lux-crypto-ntt/tests/kat.rs index 45e61d8..fb684f5 100644 --- a/rust/lux-crypto-ntt/tests/kat.rs +++ b/rust/lux-crypto-ntt/tests/kat.rs @@ -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), diff --git a/rust/lux-crypto-ntt/tests/spec_vectors.rs b/rust/lux-crypto-ntt/tests/spec_vectors.rs index bba9191..af1bbcb 100644 --- a/rust/lux-crypto-ntt/tests/spec_vectors.rs +++ b/rust/lux-crypto-ntt/tests/spec_vectors.rs @@ -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), } } diff --git a/rust/lux-crypto-poly_mul/Cargo.toml b/rust/lux-crypto-poly_mul/Cargo.toml deleted file mode 100644 index 316b8d5..0000000 --- a/rust/lux-crypto-poly_mul/Cargo.toml +++ /dev/null @@ -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 diff --git a/rust/lux-crypto-poly_mul/README.md b/rust/lux-crypto-poly_mul/README.md deleted file mode 100644 index 65f84e0..0000000 --- a/rust/lux-crypto-poly_mul/README.md +++ /dev/null @@ -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. diff --git a/rust/lux-crypto-poly_mul/build.rs b/rust/lux-crypto-poly_mul/build.rs deleted file mode 100644 index caaee4d..0000000 --- a/rust/lux-crypto-poly_mul/build.rs +++ /dev/null @@ -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++"); - } -} diff --git a/rust/lux-crypto-poly_mul/src/lib.rs b/rust/lux-crypto-poly_mul/src/lib.rs deleted file mode 100644 index 80735cf..0000000 --- a/rust/lux-crypto-poly_mul/src/lib.rs +++ /dev/null @@ -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` of length n. -#[inline] -pub fn multiply(a: &[u64], b: &[u64]) -> Result, 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) - } -} diff --git a/rust/lux-crypto-poly_mul/tests/spec_vectors.rs b/rust/lux-crypto-poly_mul/tests/spec_vectors.rs deleted file mode 100644 index 738c360..0000000 --- a/rust/lux-crypto-poly_mul/tests/spec_vectors.rs +++ /dev/null @@ -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 { - 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 = vec![1, 2, 3, 4]; - let b: Vec = vec![5, 6, 7, 8]; - let want: Vec = 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()); -}