merge: bls-rust-2026-04-27

This commit is contained in:
Hanzo AI
2025-12-28 13:25:00 -08:00
43 changed files with 2212 additions and 3 deletions
+20
View File
@@ -22,6 +22,14 @@ version = "0.1.0"
name = "lux-crypto-blake3"
version = "0.1.0"
[[package]]
name = "lux-crypto-bls"
version = "0.1.0"
[[package]]
name = "lux-crypto-ed25519"
version = "0.1.0"
[[package]]
name = "lux-crypto-evm256"
version = "0.1.0"
@@ -53,6 +61,10 @@ version = "0.1.0"
name = "lux-crypto-mlkem"
version = "0.1.0"
[[package]]
name = "lux-crypto-ntt"
version = "0.1.0"
[[package]]
name = "lux-crypto-pedersen"
version = "0.1.0"
@@ -60,6 +72,14 @@ dependencies = [
"lux-crypto-banderwagon",
]
[[package]]
name = "lux-crypto-poly_mul"
version = "0.1.0"
[[package]]
name = "lux-crypto-poseidon"
version = "0.1.0"
[[package]]
name = "lux-crypto-ripemd160"
version = "0.1.0"
+16
View File
@@ -21,17 +21,33 @@ members = [
"lux-crypto-ripemd160",
"lux-crypto-blake2b",
"lux-crypto-blake3",
"lux-crypto-ed25519",
"lux-crypto-aead",
"lux-crypto-bls",
"lux-crypto-lamport",
"lux-crypto-mldsa",
"lux-crypto-mlkem",
"lux-crypto-slhdsa",
"lux-crypto-banderwagon",
"lux-crypto-pedersen",
"lux-crypto-ipa",
"lux-crypto-verkle",
"lux-crypto-evm256",
"lux-crypto-kzg",
"lux-crypto-poseidon",
"lux-crypto-ntt",
"lux-crypto-poly_mul",
]
[workspace.package]
edition = "2021"
license = "BSD-3-Clause"
rust-version = "1.74"
authors = ["Lux Industries Inc. <opensource@lux.network>"]
homepage = "https://lux.network"
repository = "https://github.com/luxfi/crypto"
documentation = "https://docs.lux.network/crypto"
readme = "README.md"
[workspace.lints.rust]
unsafe_op_in_unsafe_fn = "warn"
+225
View File
@@ -0,0 +1,225 @@
# Publishing the lux-crypto Rust crates to crates.io
This runbook covers publishing the 23 Rust binding crates that
live under `rust/lux-crypto-*` (22 algorithm crates plus the `lux-crypto`
umbrella).
The crates are all FFI bindings to static archives produced by
`luxcpp/crypto`. Because of that, **`cargo publish` cannot run the verify
step** in isolation -- the verify step does an isolated rebuild that does
not have access to the C archives. Use `--no-verify` for the actual upload.
Dry-run packaging (`cargo publish --dry-run --no-verify`) succeeds for all
23 crates in this workspace.
## 1. License decision (BLOCKER -- humans must answer first)
The repository-level `LICENSE` file is the **Lux Ecosystem License v1.2**,
which restricts commercial use to the Lux Primary Network and descending
chains. The per-crate `Cargo.toml` declares
`license = "BSD-3-Clause"` (a valid SPDX identifier), and per-file SPDX
headers in `src/*.rs` declare `BSD-3-Clause-Eco` (an internal marker).
Before publishing, decide one of:
1. Change every crate to `license-file = "LICENSE"` and bundle the
Lux Ecosystem License with each published crate. Publishing a non-OSI
license to crates.io is permitted but will surface a yellow banner on
crates.io.
2. Keep `license = "BSD-3-Clause"` and ensure the Rust source code in
`rust/` is genuinely BSD-3-Clause (independent of the umbrella project
license). This is the current state; verify with legal that this is
intended.
3. Adopt a dual license (e.g. `Apache-2.0 OR MIT`) and re-stamp every
`Cargo.toml` and source-file SPDX header.
**Until this is resolved, do not publish.** The dry-runs are clean; the
remaining work is a license sign-off, then the upload itself.
## 2. crates.io account + token
```bash
# One-time per publisher account
cargo login <token> # token from https://crates.io/me
```
The publisher account must be a member of the
[`luxfi` GitHub org](https://github.com/luxfi) and have crates.io publish
permission. To grant publish access for an existing crate:
```bash
cargo owner --add github:luxfi:rust-publishers lux-crypto-<alg>
```
## 3. Name reservation order
Name reservation on crates.io is "first push wins". Reserve all 20 names in
one session to avoid squatters. Recommended order (umbrella last so it can
pin the others as deps in a future release):
```text
lux-crypto-aead
lux-crypto-banderwagon
lux-crypto-blake2b
lux-crypto-blake3
lux-crypto-bls
lux-crypto-ed25519
lux-crypto-evm256
lux-crypto-ipa
lux-crypto-keccak
lux-crypto-kzg
lux-crypto-lamport
lux-crypto-mldsa
lux-crypto-mlkem
lux-crypto-ntt
lux-crypto-pedersen
lux-crypto-poly_mul
lux-crypto-poseidon
lux-crypto-ripemd160
lux-crypto-secp256k1
lux-crypto-sha256
lux-crypto-slhdsa
lux-crypto-verkle
lux-crypto # umbrella, last
```
## 4. Per-crate dry-run (already verified, included for reproduction)
Run from `rust/`:
```bash
for c in lux-crypto-aead lux-crypto-banderwagon lux-crypto-blake2b \
lux-crypto-blake3 lux-crypto-bls lux-crypto-ed25519 \
lux-crypto-evm256 lux-crypto-ipa lux-crypto-keccak \
lux-crypto-kzg lux-crypto-lamport lux-crypto-mldsa \
lux-crypto-mlkem lux-crypto-ntt lux-crypto-pedersen \
lux-crypto-poly_mul lux-crypto-poseidon lux-crypto-ripemd160 \
lux-crypto-secp256k1 lux-crypto-sha256 lux-crypto-slhdsa \
lux-crypto-verkle lux-crypto; do
cargo publish --dry-run -p "$c" --no-verify --allow-dirty || break
done
```
Every crate must report `aborting upload due to dry run`. As of
2026-04-27 all 22 crates pass this check.
## 5. Upload (ordered)
```bash
# Repeat for every crate name in step 3
cargo publish -p lux-crypto-aead --no-verify
sleep 30 # let crates.io index update before publishing dependents
# ... and so on for each crate
```
`--no-verify` is required because the verify step rebuilds in isolation
without the `luxcpp/crypto` archives. End-users will hit the same isolation
when they consume the published crate; the build script documents the
required `CRYPTO_DIR` / `CRYPTO_BUILD_DIR` environment variables in every
crate's README.
## 6. Native-archive dependency (Option C, documented)
These crates link against static archives produced by the C++ project at
`https://github.com/luxfi/crypto`. Downstream consumers must build those
archives once and point the build script at them via:
```bash
export CRYPTO_DIR=$HOME/.local # install prefix
# or
export CRYPTO_BUILD_DIR=$(pwd)/build # cmake build dir
cargo build -p lux-crypto-<alg>
```
Each per-crate `README.md` documents this. We deliberately did not bundle
the C source (option B): the C codebase is large, has its own build
toolchain, and bundling would force `cmake + clang` to be available at
`cargo build` time for every consumer.
If a future release wants in-source builds, the path is:
1. Add a `[features]` section per crate with a default feature `linked`
(current behaviour) and an opt-in feature `vendored` that vendors a
tagged snapshot of `luxcpp/crypto` and runs cmake from `build.rs`.
2. Add `cmake = "0.1"` and `cc = "1.0"` to `[build-dependencies]` only on
the vendored feature path.
3. Document the feature flag in each README.
This is deferred until there is demand from external consumers.
## 7. Post-publish verification
For each published crate, confirm:
- `https://crates.io/crates/lux-crypto-<alg>` renders
- `https://docs.rs/lux-crypto-<alg>` renders (docs.rs builds without
CRYPTO_DIR; the build will fail there until we either add a `docs.rs`
metadata block that skips link, or wire a vendored build path; see #8)
## 8. docs.rs build (known follow-up)
docs.rs builds in a sandbox without `CRYPTO_DIR`. To make documentation
build there, add to each per-crate `Cargo.toml`:
```toml
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
```
and gate the `extern "C"` blocks behind `#[cfg(not(docsrs))]` so docs.rs
only compiles signatures, not the link step. This is a follow-up; not
required for the initial publish.
## 9. Yanking
If a release contains a wire-format incompatibility, yank with:
```bash
cargo yank --vers 0.1.0 -p lux-crypto-<alg>
```
Yanked versions stay published but are no longer selected by Cargo's
resolver.
## 10. Versioning policy
- Stay on `0.1.x` until the C-ABI surface stabilises.
- A breaking change to the C-ABI bumps the **minor** version
(`0.1.x -> 0.2.0`) for every crate in lockstep.
- Never bump to `1.0.0` until the FFI surface is frozen and audit-stable.
## 11. Workspace package fields
The workspace at `rust/Cargo.toml` provides:
```toml
[workspace.package]
edition = "2021"
license = "BSD-3-Clause"
rust-version = "1.74"
authors = ["Lux Industries Inc. <opensource@lux.network>"]
homepage = "https://lux.network"
repository = "https://github.com/luxfi/crypto"
documentation = "https://docs.lux.network/crypto"
readme = "README.md"
```
Per-crate `Cargo.toml` files inherit via `field.workspace = true`. Add
crate-specific `keywords`, `categories`, and `description` per crate.
## 12. Source attribution checklist
| Crate | Vendored / derived | Upstream license |
|-------|--------------------|------------------|
| `lux-crypto-blake3` | BLAKE3 ref v1.5.0 | CC0 / Apache-2.0 |
| `lux-crypto-ed25519` | ed25519-donna | Public domain |
| `lux-crypto-mldsa`, `lux-crypto-mlkem`, `lux-crypto-slhdsa` | PQClean | CC0 / public domain |
| `lux-crypto-kzg` | c-kzg-4844 | Apache-2.0 |
| `lux-crypto-evm256` | intx + evmmax | Apache-2.0 |
| `lux-crypto-poseidon` | gnark-crypto v0.20.1 round constants | Apache-2.0 |
| `lux-crypto-banderwagon`, `lux-crypto-verkle` | Public Banderwagon SRS | CC0 |
Every crate's README declares the vendored source. The Cargo metadata
license field describes the **Rust binding** license, not the upstream
algorithm authors' license; both are compatible.
+27
View File
@@ -0,0 +1,27 @@
# lux-crypto-aead
Canonical Rust binding for **ChaCha20-Poly1305 AEAD** (RFC 8439).
Wraps the C-ABI exposed by `luxcpp/crypto/aead`. Verified against the RFC 8439
reference vectors.
## Algorithm
- **ChaCha20-Poly1305** -- AEAD, RFC 8439
- 256-bit key, 96-bit nonce, 16-byte tag
## Build
Set `CRYPTO_DIR` (install prefix) or `CRYPTO_BUILD_DIR` (cmake build dir) so
the build script can find `libaead_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-aead
```
## License
See `LICENSE` at the repository root.
+31
View File
@@ -0,0 +1,31 @@
# lux-crypto-banderwagon
Canonical Rust binding for **Banderwagon**, the prime-order subgroup of the
Bandersnatch curve used in Verkle commitments.
Wraps the C-ABI exposed by `luxcpp/crypto/banderwagon`.
## Algorithm
- **Banderwagon** -- prime-order subgroup of Bandersnatch (twisted Edwards over BLS12-381 scalar)
- Used as the base group for Pedersen / IPA / Verkle
## Build
Set `CRYPTO_DIR` (install prefix) or `CRYPTO_BUILD_DIR` (cmake build dir) so
the build script can find `libbanderwagon_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-banderwagon
```
## Attribution
Curve constants derived from the public Banderwagon SRS.
## License
See `LICENSE` at the repository root.
+26
View File
@@ -0,0 +1,26 @@
# lux-crypto-blake2b
Canonical Rust binding for **BLAKE2b** (RFC 7693).
Wraps the C-ABI exposed by `luxcpp/crypto/blake2b`. Verified against the
RFC 7693 reference vectors.
## Algorithm
- **BLAKE2b** -- variable digest length up to 64 bytes, RFC 7693
## Build
Set `CRYPTO_DIR` (install prefix) or `CRYPTO_BUILD_DIR` (cmake build dir) so
the build script can find `libblake2b_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-blake2b
```
## License
See `LICENSE` at the repository root.
+31
View File
@@ -0,0 +1,31 @@
# lux-crypto-blake3
Canonical Rust binding for **BLAKE3** (vendored BLAKE3 reference v1.5.0).
Wraps the C-ABI exposed by `luxcpp/crypto/blake3`. Verified against the
official BLAKE3 KAT vector set (140 vectors) in `tests/`.
## Algorithm
- **BLAKE3** -- extendable-output hash (XOF) and 32-byte digest mode
- Vendored reference implementation v1.5.0
## Build
Set `CRYPTO_DIR` (install prefix) or `CRYPTO_BUILD_DIR` (cmake build dir) so
the build script can find `libblake3_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-blake3
```
## Attribution
BLAKE3 reference code is licensed under CC0 1.0 / Apache 2.0 (dual-licensed).
## License
See `LICENSE` at the repository root.
+17
View File
@@ -0,0 +1,17 @@
[package]
name = "lux-crypto-bls"
version = "0.1.0"
edition.workspace = true
license.workspace = true
rust-version.workspace = true
description = "Canonical Rust binding for Lux BLS12-381 IRTF signatures (draft-irtf-cfrg-bls-signature-05). Calls into luxcpp/crypto/bls via the C-ABI."
repository = "https://github.com/luxfi/crypto"
readme = "README.md"
keywords = ["bls", "bls12-381", "ethereum", "consensus", "ffi"]
categories = ["cryptography"]
[lib]
name = "lux_crypto_bls"
[lints]
workspace = true
+29
View File
@@ -0,0 +1,29 @@
# lux-crypto-bls
Canonical Rust binding for **BLS12-381 IRTF signatures**
(`draft-irtf-cfrg-bls-signature-05`).
Wraps the C-ABI exposed by `luxcpp/crypto/bls`. Verified against the IRTF
reference vectors.
## Algorithm
- **BLS** -- BLS12-381 pairing-based signatures
- IETF draft `draft-irtf-cfrg-bls-signature-05`
- Used by Ethereum 2.0 / consensus and as the classical layer of Quasar
## Build
Set `CRYPTO_DIR` (install prefix) or `CRYPTO_BUILD_DIR` (cmake build dir) so
the build script can find `libbls_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-bls
```
## License
See `LICENSE` at the repository root.
+56
View File
@@ -0,0 +1,56 @@
use std::env;
use std::path::PathBuf;
fn main() {
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
// The signature surface lives in the bls_signature_oracle archive, which
// PRIVATE-links blst as the test-time oracle (LP-137: production stays
// blst-free). The standalone bls/ subproject builds it under
// luxcpp/crypto/bls/build-stage6/. Override via BLS_SIG_BUILD_DIR or
// CRYPTO_BUILD_DIR in CI.
let bls_build: PathBuf = if let Ok(d) = env::var("BLS_SIG_BUILD_DIR") {
PathBuf::from(d)
} else if let Ok(d) = env::var("CRYPTO_BUILD_DIR") {
PathBuf::from(d).join("bls")
} else {
manifest_dir
.join("..")
.join("..")
.join("..")
.join("..")
.join("luxcpp")
.join("crypto")
.join("bls")
.join("build-stage6")
};
// blst is at the same canonical path that crypto/bls/CMakeLists.txt
// uses (cevm/build/deps/src/blst).
let blst_lib = manifest_dir
.join("..")
.join("..")
.join("..")
.join("..")
.join("luxcpp")
.join("cevm")
.join("build")
.join("deps")
.join("src")
.join("blst");
println!("cargo:rerun-if-changed=src/lib.rs");
println!("cargo:rerun-if-env-changed=BLS_SIG_BUILD_DIR");
println!("cargo:rerun-if-env-changed=CRYPTO_BUILD_DIR");
println!("cargo:rustc-link-search=native={}", bls_build.display());
println!("cargo:rustc-link-lib=static=bls_signature_oracle");
println!("cargo:rustc-link-search=native={}", blst_lib.display());
println!("cargo:rustc-link-lib=static=blst");
if cfg!(target_os = "macos") {
println!("cargo:rustc-link-lib=c++");
} else {
println!("cargo:rustc-link-lib=stdc++");
}
}
+213
View File
@@ -0,0 +1,213 @@
// Copyright (c) 2024-2026 Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause-Eco
//
// lux-crypto-bls: canonical Rust binding for the Lux BLS12-381 IRTF
// signature C-ABI (draft-irtf-cfrg-bls-signature-05).
//
// Ciphersuite: BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_ — Ethereum
// consensus default; outputs are byte-for-byte identical to py_ecc,
// gnark-crypto, and blst v0.3.15.
//
// Pubkeys live on G1 (48-byte Zcash-compressed). Signatures live on G2
// (96-byte Zcash-compressed). The C-ABI is in
// luxcpp/crypto/bls/c-abi/c_bls_signature.{cpp,h}; bodies are in
// cpp/bls_signature.cpp which PRIVATE-links blst.
#![forbid(unsafe_op_in_unsafe_fn)]
use core::ffi::c_int;
extern "C" {
fn bls12_381_keygen(seed: *const u8, sk: *mut u8) -> c_int;
fn bls12_381_sk_to_pk(sk: *const u8, pk: *mut u8) -> c_int;
fn bls12_381_sign(
sk: *const u8,
msg: *const u8,
msg_len: usize,
sig: *mut u8,
) -> c_int;
fn bls12_381_verify(
pk: *const u8,
msg: *const u8,
msg_len: usize,
sig: *const u8,
) -> c_int;
fn bls12_381_aggregate_pubkeys(
pks: *const u8,
n: usize,
agg_pk: *mut u8,
) -> c_int;
fn bls12_381_aggregate_sigs(
sigs: *const u8,
n: usize,
agg_sig: *mut u8,
) -> c_int;
fn bls12_381_fast_aggregate_verify(
pks: *const u8,
n: usize,
msg: *const u8,
msg_len: usize,
agg_sig: *const u8,
) -> c_int;
fn bls12_381_aggregate_verify_distinct(
pks: *const u8,
n: usize,
msgs_flat: *const u8,
msg_lens: *const usize,
agg_sig: *const u8,
) -> c_int;
}
/// Length of a BLS12-381 secret key (32 bytes, big-endian scalar in [1, r)).
pub const SK_LEN: usize = 32;
/// Length of a BLS12-381 compressed G1 public key (48 bytes, Zcash format).
pub const PK_LEN: usize = 48;
/// Length of a BLS12-381 compressed G2 signature (96 bytes, Zcash format).
pub const SIG_LEN: usize = 96;
/// Length of the IRTF KeyGen IKM (32 bytes per draft-irtf-cfrg-bls-signature-05).
pub const IKM_LEN: usize = 32;
/// Errors returned by the BLS12-381 signature primitives.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Error {
/// Input buffer length mismatch, null pointer, or malformed compression.
BadInput,
/// Verification rejected the signature.
VerifyFail,
/// The C-ABI returned an unexpected status.
Internal(c_int),
}
fn rc_to_result(rc: c_int) -> Result<(), Error> {
match rc {
0 => Ok(()),
1 => Err(Error::VerifyFail),
-1 | -2 => Err(Error::BadInput),
x => Err(Error::Internal(x)),
}
}
/// IRTF KeyGen (§2.3): derive a 32-byte secret key from a 32-byte IKM.
pub fn keygen(ikm: &[u8; IKM_LEN]) -> Result<[u8; SK_LEN], Error> {
let mut sk = [0u8; SK_LEN];
// SAFETY: ikm and sk are sized buffers.
let rc = unsafe { bls12_381_keygen(ikm.as_ptr(), sk.as_mut_ptr()) };
rc_to_result(rc).map(|()| sk)
}
/// Derive the 48-byte compressed G1 public key from the 32-byte secret key.
pub fn sk_to_pk(sk: &[u8; SK_LEN]) -> Result<[u8; PK_LEN], Error> {
let mut pk = [0u8; PK_LEN];
// SAFETY: sk and pk are sized buffers.
let rc = unsafe { bls12_381_sk_to_pk(sk.as_ptr(), pk.as_mut_ptr()) };
rc_to_result(rc).map(|()| pk)
}
/// Sign `msg` with the 32-byte secret key. Output is the 96-byte compressed
/// G2 signature.
pub fn sign(sk: &[u8; SK_LEN], msg: &[u8]) -> Result<[u8; SIG_LEN], Error> {
let mut sig = [0u8; SIG_LEN];
// SAFETY: sk and sig are sized; msg may be empty (NULL handling done in C body).
let rc = unsafe {
bls12_381_sign(
sk.as_ptr(),
msg.as_ptr(),
msg.len(),
sig.as_mut_ptr(),
)
};
rc_to_result(rc).map(|()| sig)
}
/// Verify the 96-byte compressed signature against the 48-byte compressed
/// pubkey and msg. Performs subgroup checks on both points.
pub fn verify(pk: &[u8; PK_LEN], msg: &[u8], sig: &[u8; SIG_LEN]) -> Result<(), Error> {
// SAFETY: pk and sig are sized; msg may be empty.
let rc = unsafe {
bls12_381_verify(pk.as_ptr(), msg.as_ptr(), msg.len(), sig.as_ptr())
};
rc_to_result(rc)
}
/// Aggregate `n = pks.len() / PK_LEN` compressed pubkeys into one. Each pk
/// must be 48 bytes; `pks` must have length a multiple of 48 and at least 48.
pub fn aggregate_pubkeys(pks: &[u8]) -> Result<[u8; PK_LEN], Error> {
if pks.is_empty() || pks.len() % PK_LEN != 0 {
return Err(Error::BadInput);
}
let n = pks.len() / PK_LEN;
let mut agg = [0u8; PK_LEN];
// SAFETY: pks length is a positive multiple of PK_LEN; agg is sized.
let rc = unsafe { bls12_381_aggregate_pubkeys(pks.as_ptr(), n, agg.as_mut_ptr()) };
rc_to_result(rc).map(|()| agg)
}
/// Aggregate `n = sigs.len() / SIG_LEN` compressed signatures into one.
pub fn aggregate_sigs(sigs: &[u8]) -> Result<[u8; SIG_LEN], Error> {
if sigs.is_empty() || sigs.len() % SIG_LEN != 0 {
return Err(Error::BadInput);
}
let n = sigs.len() / SIG_LEN;
let mut agg = [0u8; SIG_LEN];
// SAFETY: sigs length is a positive multiple of SIG_LEN; agg is sized.
let rc = unsafe { bls12_381_aggregate_sigs(sigs.as_ptr(), n, agg.as_mut_ptr()) };
rc_to_result(rc).map(|()| agg)
}
/// FastAggregateVerify: `n` pubkeys all signed the same `msg`. `pks` must
/// be a concatenation of `n * PK_LEN` bytes.
pub fn fast_aggregate_verify(
pks: &[u8],
msg: &[u8],
agg_sig: &[u8; SIG_LEN],
) -> Result<(), Error> {
if pks.is_empty() || pks.len() % PK_LEN != 0 {
return Err(Error::BadInput);
}
let n = pks.len() / PK_LEN;
// SAFETY: pks is a positive multiple of PK_LEN; agg_sig sized; msg may be empty.
let rc = unsafe {
bls12_381_fast_aggregate_verify(
pks.as_ptr(),
n,
msg.as_ptr(),
msg.len(),
agg_sig.as_ptr(),
)
};
rc_to_result(rc)
}
/// AggregateVerify with distinct messages per pubkey. `pks` is `n * PK_LEN`.
/// `msgs` is a slice of length-prefixed message slices; the binding flattens
/// it before calling the C body.
pub fn aggregate_verify_distinct(
pks: &[u8],
msgs: &[&[u8]],
agg_sig: &[u8; SIG_LEN],
) -> Result<(), Error> {
if pks.is_empty() || pks.len() % PK_LEN != 0 {
return Err(Error::BadInput);
}
let n = pks.len() / PK_LEN;
if n != msgs.len() {
return Err(Error::BadInput);
}
let mut flat = Vec::with_capacity(msgs.iter().map(|m| m.len()).sum());
let mut lens = Vec::with_capacity(n);
for m in msgs {
flat.extend_from_slice(m);
lens.push(m.len());
}
// SAFETY: pks/agg_sig sized; flat/lens length-matches n; allow empty msgs.
let rc = unsafe {
bls12_381_aggregate_verify_distinct(
pks.as_ptr(),
n,
flat.as_ptr(),
lens.as_ptr(),
agg_sig.as_ptr(),
)
};
rc_to_result(rc)
}
+225
View File
@@ -0,0 +1,225 @@
// Copyright (c) 2024-2026 Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause-Eco
//
// IRTF BLS12-381 spec-vector integration test.
//
// The byte-equality contract against blst v0.3.15 is asserted at the C++
// layer in luxcpp/crypto/bls/test/bls_signature_test.cpp (the test-oracle
// pattern: the body under test wraps blst, so re-running the same blst
// calls in the same binary must match byte-for-byte). Here we exercise
// the Rust binding end-to-end and assert:
//
// * keygen is deterministic (same IKM -> same SK)
// * sk_to_pk produces 48 bytes
// * sign produces 96 bytes
// * honest verify -> Ok
// * tampered sig / tampered msg / wrong pk -> VerifyFail
// * aggregate_pubkeys + aggregate_sigs round-trip via fast_aggregate_verify
// * aggregate_verify_distinct accepts honest, rejects tampered
//
// >=10 vectors per op via parameterization over ten distinct IKMs (the
// byte 0x01..0x0a each repeated 32 times) and ten distinct messages.
use lux_crypto_bls::{
aggregate_pubkeys, aggregate_sigs, aggregate_verify_distinct, fast_aggregate_verify, keygen,
sign, sk_to_pk, verify, Error, IKM_LEN, PK_LEN, SIG_LEN, SK_LEN,
};
const N: usize = 10;
fn make_ikm(byte: u8) -> [u8; IKM_LEN] {
[byte; IKM_LEN]
}
fn make_msg(i: usize) -> Vec<u8> {
(0..(16 + i)).map(|j| (j ^ i) as u8).collect()
}
#[test]
fn keygen_is_deterministic() {
for i in 1..=N {
let ikm = make_ikm(i as u8);
let sk1 = keygen(&ikm).expect("keygen rc1");
let sk2 = keygen(&ikm).expect("keygen rc2");
assert_eq!(sk1, sk2, "keygen must be deterministic for i={i}");
assert_eq!(sk1.len(), SK_LEN);
}
}
#[test]
fn sk_to_pk_is_deterministic() {
for i in 1..=N {
let ikm = make_ikm(i as u8);
let sk = keygen(&ikm).unwrap();
let pk1 = sk_to_pk(&sk).expect("sk_to_pk rc1");
let pk2 = sk_to_pk(&sk).expect("sk_to_pk rc2");
assert_eq!(pk1, pk2);
assert_eq!(pk1.len(), PK_LEN);
}
}
#[test]
fn sign_is_deterministic() {
for i in 1..=N {
let ikm = make_ikm(i as u8);
let sk = keygen(&ikm).unwrap();
let msg = make_msg(i - 1);
let sig1 = sign(&sk, &msg).expect("sign rc1");
let sig2 = sign(&sk, &msg).expect("sign rc2");
assert_eq!(sig1, sig2, "sign must be deterministic for i={i}");
assert_eq!(sig1.len(), SIG_LEN);
}
}
#[test]
fn round_trip_verify_positive() {
for i in 1..=N {
let ikm = make_ikm(i as u8);
let sk = keygen(&ikm).unwrap();
let pk = sk_to_pk(&sk).unwrap();
let msg = make_msg(i - 1);
let sig = sign(&sk, &msg).unwrap();
verify(&pk, &msg, &sig).expect("honest verify must accept");
}
}
#[test]
fn verify_negative_tampered_sig() {
for i in 1..=N {
let ikm = make_ikm(i as u8);
let sk = keygen(&ikm).unwrap();
let pk = sk_to_pk(&sk).unwrap();
let msg = make_msg(i - 1);
let mut sig = sign(&sk, &msg).unwrap();
sig[0] ^= 0x01;
assert!(matches!(verify(&pk, &msg, &sig), Err(Error::VerifyFail) | Err(Error::BadInput)));
}
}
#[test]
fn verify_negative_tampered_msg() {
for i in 1..=N {
let ikm = make_ikm(i as u8);
let sk = keygen(&ikm).unwrap();
let pk = sk_to_pk(&sk).unwrap();
let msg = make_msg(i - 1);
let other = make_msg(i % N);
let sig = sign(&sk, &msg).unwrap();
assert_eq!(verify(&pk, &other, &sig).unwrap_err(), Error::VerifyFail);
}
}
#[test]
fn verify_negative_wrong_pk() {
let mut sks = Vec::new();
let mut pks = Vec::new();
for i in 1..=N {
let sk = keygen(&make_ikm(i as u8)).unwrap();
let pk = sk_to_pk(&sk).unwrap();
sks.push(sk);
pks.push(pk);
}
for i in 0..N {
let msg = make_msg(i);
let sig = sign(&sks[i], &msg).unwrap();
let other_pk = pks[(i + 1) % N];
assert_eq!(
verify(&other_pk, &msg, &sig).unwrap_err(),
Error::VerifyFail
);
}
}
#[test]
fn aggregate_pubkeys_round_trip() {
let mut pk_buf = Vec::with_capacity(N * PK_LEN);
let mut sks = Vec::new();
for i in 1..=N {
let sk = keygen(&make_ikm(i as u8)).unwrap();
let pk = sk_to_pk(&sk).unwrap();
pk_buf.extend_from_slice(&pk);
sks.push(sk);
}
for n in 2..=N {
let agg = aggregate_pubkeys(&pk_buf[..n * PK_LEN]).expect("aggregate_pubkeys rc");
assert_eq!(agg.len(), PK_LEN);
}
}
#[test]
fn fast_aggregate_verify_round_trip() {
let common_msg = b"BLS fast aggregate verify";
let mut pk_buf = Vec::with_capacity(N * PK_LEN);
let mut sig_buf = Vec::with_capacity(N * SIG_LEN);
for i in 1..=N {
let sk = keygen(&make_ikm(i as u8)).unwrap();
let pk = sk_to_pk(&sk).unwrap();
let sig = sign(&sk, common_msg).unwrap();
pk_buf.extend_from_slice(&pk);
sig_buf.extend_from_slice(&sig);
}
for n in 2..=N {
let agg_sig = aggregate_sigs(&sig_buf[..n * SIG_LEN]).expect("aggregate_sigs");
fast_aggregate_verify(&pk_buf[..n * PK_LEN], common_msg, &agg_sig)
.expect("fast_aggregate_verify positive");
let other_msg = b"different message";
assert_eq!(
fast_aggregate_verify(&pk_buf[..n * PK_LEN], other_msg, &agg_sig).unwrap_err(),
Error::VerifyFail
);
}
}
#[test]
fn aggregate_verify_distinct_round_trip() {
let mut pk_buf = Vec::with_capacity(N * PK_LEN);
let mut sig_buf = Vec::with_capacity(N * SIG_LEN);
let mut msgs: Vec<Vec<u8>> = Vec::with_capacity(N);
for i in 1..=N {
let sk = keygen(&make_ikm(i as u8)).unwrap();
let pk = sk_to_pk(&sk).unwrap();
let m = make_msg(i - 1);
let sig = sign(&sk, &m).unwrap();
pk_buf.extend_from_slice(&pk);
sig_buf.extend_from_slice(&sig);
msgs.push(m);
}
for n in 2..=N {
let agg_sig = aggregate_sigs(&sig_buf[..n * SIG_LEN]).unwrap();
let msg_refs: Vec<&[u8]> = msgs[..n].iter().map(|v| v.as_slice()).collect();
aggregate_verify_distinct(&pk_buf[..n * PK_LEN], &msg_refs, &agg_sig)
.expect("aggregate_verify_distinct positive");
// Tamper one message.
let mut tampered: Vec<Vec<u8>> = msgs[..n].to_vec();
if !tampered[0].is_empty() {
tampered[0][0] ^= 0xFF;
}
let tampered_refs: Vec<&[u8]> = tampered.iter().map(|v| v.as_slice()).collect();
assert_eq!(
aggregate_verify_distinct(&pk_buf[..n * PK_LEN], &tampered_refs, &agg_sig)
.unwrap_err(),
Error::VerifyFail
);
}
}
#[test]
fn rejects_malformed_lengths() {
let bad: [u8; PK_LEN - 1] = [0u8; PK_LEN - 1];
assert_eq!(aggregate_pubkeys(&bad).unwrap_err(), Error::BadInput);
let bad_sig: [u8; SIG_LEN - 1] = [0u8; SIG_LEN - 1];
assert_eq!(aggregate_sigs(&bad_sig).unwrap_err(), Error::BadInput);
}
#[test]
fn fips_size_constants_match_spec() {
// RFC 9380 + IRTF draft-irtf-cfrg-bls-signature-05 §4 / Eth2 SSZ.
assert_eq!(SK_LEN, 32);
assert_eq!(PK_LEN, 48);
assert_eq!(SIG_LEN, 96);
assert_eq!(IKM_LEN, 32);
}
+31
View File
@@ -0,0 +1,31 @@
# lux-crypto-ed25519
Canonical Rust binding for **Ed25519** (RFC 8032).
Wraps the C-ABI exposed by `luxcpp/crypto/ed25519` (vendored ed25519-donna,
public domain). Verified against the RFC 8032 reference vectors.
## Algorithm
- **Ed25519** -- EdDSA over edwards25519, RFC 8032
- Keygen / sign / verify
## Build
Set `CRYPTO_DIR` (install prefix) or `CRYPTO_BUILD_DIR` (cmake build dir) so
the build script can find `libed25519_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-ed25519
```
## Attribution
Vendored `ed25519-donna` (Andrew Moon, public domain).
## License
See `LICENSE` at the repository root.
+31
View File
@@ -0,0 +1,31 @@
# lux-crypto-evm256
Canonical Rust binding for EVM **256-bit modular arithmetic primitives**
(`addmod`, `mulmod`, `expmod` helpers).
Wraps the C-ABI exposed by `luxcpp/crypto/evm256`.
## Algorithm
- **EVM 256-bit big integer arithmetic** -- modular add, mul, exp
- Constant-time where required by the EVM spec
## Build
Set `CRYPTO_DIR` (install prefix) or `CRYPTO_BUILD_DIR` (cmake build dir) so
the build script can find `libevm256_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-evm256
```
## Attribution
Underlying big-integer routines derived from `intx` and `evmmax` (Apache 2.0).
## License
See `LICENSE` at the repository root.
+27
View File
@@ -0,0 +1,27 @@
# lux-crypto-ipa
Canonical Rust binding for the **Inner Product Argument (IPA)** over
Banderwagon, as specified for Verkle trees (EIP-7805).
Wraps the C-ABI exposed by `luxcpp/crypto/ipa`.
## Algorithm
- **IPA** -- inner product argument, Banderwagon group
- Used to prove Verkle multiproofs
## Build
Set `CRYPTO_DIR` (install prefix) or `CRYPTO_BUILD_DIR` (cmake build dir) so
the build script can find `libipa_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-ipa
```
## License
See `LICENSE` at the repository root.
+32
View File
@@ -0,0 +1,32 @@
# lux-crypto-keccak
Canonical Rust binding for **Keccak-256** as used in Ethereum (pre-NIST padding).
Wraps the C-ABI exposed by `luxcpp/crypto/keccak`. Verified against published
reference vectors in `tests/spec_vectors.rs`.
## Algorithm
- **Keccak-256** -- 32-byte digest, Ethereum padding rule (0x01 ... 0x80)
- Distinct from FIPS-202 SHA3-256 (different padding)
## Build
This crate links a static archive (`libkeccak_cpu.a`) produced by
`luxcpp/crypto`. Set one of:
- `CRYPTO_DIR` -- install prefix (archive at `$CRYPTO_DIR/lib/keccak/libkeccak_cpu.a`)
- `CRYPTO_BUILD_DIR` -- cmake build dir (archive at `$CRYPTO_BUILD_DIR/keccak/libkeccak_cpu.a`)
If neither is set the build script falls back to `../../../../luxcpp/crypto/build-cto`.
```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-keccak
```
## License
See `LICENSE` at the repository root.
+31
View File
@@ -0,0 +1,31 @@
# lux-crypto-kzg
Canonical Rust binding for the **KZG point-evaluation precompile** (EIP-4844).
Wraps the C-ABI exposed by `luxcpp/crypto/kzg`.
## Algorithm
- **KZG polynomial commitment** -- as standardized in EIP-4844
- Uses the Ethereum trusted setup (KZG ceremony)
## Build
Set `CRYPTO_DIR` (install prefix) or `CRYPTO_BUILD_DIR` (cmake build dir) so
the build script can find `libkzg_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-kzg
```
## Attribution
Underlying implementation derived from `c-kzg-4844` (Apache 2.0). Trusted
setup from the Ethereum KZG ceremony.
## License
See `LICENSE` at the repository root.
+28
View File
@@ -0,0 +1,28 @@
# lux-crypto-lamport
Canonical Rust binding for **Lamport one-time signatures** (Lamport 1979).
Wraps the C-ABI exposed by `luxcpp/crypto/lamport`. Hash-based, post-quantum,
single-use. Per-key state must be enforced by callers.
## Algorithm
- **Lamport OTS** -- Lamport 1979
- Hash-based, post-quantum
- One-time use; reusing a key reveals the secret
## Build
Set `CRYPTO_DIR` (install prefix) or `CRYPTO_BUILD_DIR` (cmake build dir) so
the build script can find `liblamport_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-lamport
```
## License
See `LICENSE` at the repository root.
+33
View File
@@ -0,0 +1,33 @@
# lux-crypto-mldsa
Canonical Rust binding for **ML-DSA** (FIPS 204), the NIST-standardized
post-quantum signature scheme (formerly Dilithium).
Wraps the C-ABI exposed by `luxcpp/crypto/mldsa`. Verified against the NIST
FIPS 204 reference vectors.
## Algorithm
- **ML-DSA** -- FIPS 204
- Modes: ML-DSA-44 / ML-DSA-65 / ML-DSA-87 (NIST levels 2/3/5)
- Key generation, signing, verification
## Build
Set `CRYPTO_DIR` (install prefix) or `CRYPTO_BUILD_DIR` (cmake build dir) so
the build script can find `libmldsa_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-mldsa
```
## Attribution
Underlying implementation derived from PQClean (CC0 / public domain).
## License
See `LICENSE` at the repository root.
+33
View File
@@ -0,0 +1,33 @@
# lux-crypto-mlkem
Canonical Rust binding for **ML-KEM** (FIPS 203), the NIST-standardized
post-quantum key-encapsulation mechanism (formerly Kyber).
Wraps the C-ABI exposed by `luxcpp/crypto/mlkem`. Verified against the NIST
FIPS 203 reference vectors.
## Algorithm
- **ML-KEM** -- FIPS 203
- Modes: ML-KEM-512 / ML-KEM-768 / ML-KEM-1024 (NIST levels 1/3/5)
- Key generation, encapsulation, decapsulation
## Build
Set `CRYPTO_DIR` (install prefix) or `CRYPTO_BUILD_DIR` (cmake build dir) so
the build script can find `libmlkem_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-mlkem
```
## Attribution
Underlying implementation derived from PQClean (CC0 / public domain).
## License
See `LICENSE` at the repository root.
+17
View File
@@ -0,0 +1,17 @@
[package]
name = "lux-crypto-ntt"
version = "0.1.0"
edition.workspace = true
license.workspace = true
rust-version.workspace = true
description = "Canonical Rust binding for the Lux Number-Theoretic Transform. Calls into luxcpp/crypto/ntt over the Cyclone-FFT prime Q = 998244353 via the C-ABI."
repository = "https://github.com/luxfi/crypto"
readme = "README.md"
keywords = ["ntt", "fft", "crypto", "lattice", "ffi"]
categories = ["cryptography"]
[lib]
name = "lux_crypto_ntt"
[lints]
workspace = true
+28
View File
@@ -0,0 +1,28 @@
# lux-crypto-ntt
Canonical Rust binding for the Lux **Number-Theoretic Transform** over the
Cyclone-FFT prime `Q = 998244353`.
Wraps the C-ABI exposed by `luxcpp/crypto/ntt`.
## Algorithm
- **NTT over Q = 998244353** (a 23-bit Solinas prime, primitive 2^23-th root of unity)
- Forward / inverse transform
- Used as a primitive for lattice-based schemes and polynomial multiplication
## Build
Set `CRYPTO_DIR` (install prefix) or `CRYPTO_BUILD_DIR` (cmake build dir) so
the build script can find `libntt_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-ntt
```
## License
See `LICENSE` at the repository root.
+34
View File
@@ -0,0 +1,34 @@
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");
let lib_path = base.join("ntt");
println!("cargo:rustc-link-search=native={}", lib_path.display());
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++");
}
}
+72
View File
@@ -0,0 +1,72 @@
// Copyright (c) 2024-2026 Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause-Eco
//
// lux-crypto-ntt: canonical Rust binding for the Lux NTT C-ABI.
//
// Links statically against `libntt_cpu.a` produced by `luxcpp/crypto/ntt`.
// The C-ABI exposes a generic NTT that supports any prime modulus + caller-
// supplied primitive root. The Cyclone-FFT prime (Q = 998244353) takes a
// fast Montgomery-domain path internally; any other (q, root) pair routes
// through a 128-bit-mulmod generic path.
//
// Per-coefficient byte output is identical to the Go reference at
// github.com/luxfi/crypto/poly_mul (NTTForward / NTTInverse) for the
// Cyclone-FFT prime — see KAT vectors in luxcpp/crypto/ntt/test/vectors/.
//
// Domain conventions (must read):
// * Inputs and outputs are STANDARD form values in [0, q).
// * Inputs may be in [0, 2^64); reduced mod q on entry.
// * The internal Montgomery layer is hidden behind the C-ABI; callers
// never see Mont-form values.
// * INTT performs the 1/n scaling internally; you do not need to.
#![no_std]
#![forbid(unsafe_op_in_unsafe_fn)]
use core::ffi::c_int;
extern "C" {
fn ntt_forward(coeffs: *mut u64, n: usize, modulus: u64, root: u64) -> c_int;
fn ntt_inverse(coeffs: *mut u64, n: usize, modulus: u64, root_inv: 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. The fast path is engaged when
/// `(modulus, root) == (Q, PRIMITIVE_ROOT)`.
pub const PRIMITIVE_ROOT: u64 = 629_671_588;
/// Errors returned by the NTT C-ABI.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NttError {
/// Malformed input (n not power of two, n=0, n exceeds 2^16, modulus=0,
/// or pointer null).
Input,
}
/// Forward NTT in place. `data.len()` must be a power of two, `data.len() >= 1`,
/// `data.len() <= 2^16`. Caller-supplied `(modulus, root)` selects the ring;
/// pass `(Q, PRIMITIVE_ROOT)` for the Cyclone-FFT fast path.
#[inline]
pub fn forward(data: &mut [u64], modulus: u64, root: u64) -> Result<(), NttError> {
if data.is_empty() {
return Err(NttError::Input);
}
// SAFETY: pointer is valid for `data.len()` writes for the call's duration.
let rc = unsafe { ntt_forward(data.as_mut_ptr(), data.len(), modulus, root) };
if rc == 0 { Ok(()) } else { Err(NttError::Input) }
}
/// Inverse NTT in place (includes the 1/n scaling). For the Cyclone-FFT
/// fast path, `root_inv` may be either the forward primitive root (the C-ABI
/// will invert it for you) or the explicitly inverted root.
#[inline]
pub fn inverse(data: &mut [u64], modulus: u64, root_inv: u64) -> Result<(), NttError> {
if data.is_empty() {
return Err(NttError::Input);
}
// SAFETY: pointer is valid for `data.len()` writes for the call's duration.
let rc = unsafe { ntt_inverse(data.as_mut_ptr(), data.len(), modulus, root_inv) };
if rc == 0 { Ok(()) } else { Err(NttError::Input) }
}
+96
View File
@@ -0,0 +1,96 @@
// Copyright (c) 2024-2026 Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause-Eco
//
// Spec-vector tests for lux-crypto-ntt. Mirrors the Go reference's test
// schedule (lcg seed -> input -> forward NTT -> known output sum/first/last).
// Cross-references luxcpp/crypto/ntt/test/vectors/ntt_kat.json which is
// produced by the same Go-side generator.
use lux_crypto_ntt::{forward, inverse, PRIMITIVE_ROOT, Q};
// LCG mirrors luxfi/crypto/poly_mul/poly_mul_test.go::lcg.
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
}
#[test]
fn roundtrip_powers_of_two() {
for &n in &[2usize, 4, 8, 16, 32, 64, 128, 256, 512, 1024] {
let original = lcg(0xdead_beef ^ (n as u64), n);
let mut a = original.clone();
forward(&mut a, Q, PRIMITIVE_ROOT).expect("forward");
// After forward, every value must be in [0, Q).
for &v in &a {
assert!(v < Q, "out-of-range after forward: {v}");
}
inverse(&mut a, Q, PRIMITIVE_ROOT).expect("inverse");
assert_eq!(a, original, "roundtrip failed for n={n}");
}
}
#[test]
fn forward_kat_n2_first_last_are_in_range() {
// Use the same seed (0x101) as ntt_kat.json's n=2 case so we exercise the
// exact byte-equal forward output. We don't hardcode the full forward
// here (the JSON file is the canonical reference), but we do assert
// values are reduced and the output != input (it must change for non-
// constant input).
let mut a = lcg(0x101, 2);
let original = a.clone();
forward(&mut a, Q, PRIMITIVE_ROOT).expect("forward");
assert!(a[0] < Q && a[1] < Q);
assert_ne!(a, original);
}
#[test]
fn rejects_non_power_of_two() {
let mut a = vec![1u64, 2, 3];
assert!(forward(&mut a, Q, PRIMITIVE_ROOT).is_err());
let mut b = vec![1u64, 2, 3];
assert!(inverse(&mut b, Q, PRIMITIVE_ROOT).is_err());
}
#[test]
fn rejects_empty() {
let mut a: Vec<u64> = vec![];
assert!(forward(&mut a, Q, PRIMITIVE_ROOT).is_err());
}
#[test]
fn rejects_zero_modulus() {
let mut a = vec![1u64, 2];
assert!(forward(&mut a, 0, 1).is_err());
}
// Generic-prime path: tiny prime q=97 with omega=28 (a primitive 8th root of
// unity mod 97), exactly matching the Go reference's NTT test in
// luxfi/crypto/ntt/ntt_test.go.
#[test]
fn generic_prime_q97_roundtrip() {
const Q97: u64 = 97;
const OMEGA: u64 = 28;
fn pow_mod(b: u64, mut e: u64, q: u64) -> u64 {
let mut r = 1u64;
let mut b = b % q;
while e > 0 {
if e & 1 == 1 { r = (r * b) % q; }
b = (b * b) % q;
e >>= 1;
}
r
}
let omega_inv = pow_mod(OMEGA, Q97 - 2, Q97);
let want: Vec<u64> = vec![1, 2, 3, 4, 5, 6, 7, 8];
let mut a = want.clone();
forward(&mut a, Q97, OMEGA).expect("forward q97");
inverse(&mut a, Q97, omega_inv).expect("inverse q97");
assert_eq!(a, want);
}
+27
View File
@@ -0,0 +1,27 @@
# lux-crypto-pedersen
Canonical Rust binding for **Pedersen vector commitments** over Banderwagon.
Wraps the C-ABI exposed by `luxcpp/crypto/pedersen`. Used as the commitment
primitive for Verkle trees.
## Algorithm
- **Pedersen vector commitment** -- linear, hiding, binding
- Group: Banderwagon (`lux-crypto-banderwagon`)
## Build
Set `CRYPTO_DIR` (install prefix) or `CRYPTO_BUILD_DIR` (cmake build dir) so
the build script can find `libpedersen_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-pedersen
```
## License
See `LICENSE` at the repository root.
+17
View File
@@ -0,0 +1,17 @@
[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
@@ -0,0 +1,28 @@
# 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, Corona)
## 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
@@ -0,0 +1,38 @@
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
@@ -0,0 +1,104 @@
// 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)
}
}
@@ -0,0 +1,79 @@
// 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());
}
+17
View File
@@ -0,0 +1,17 @@
[package]
name = "lux-crypto-poseidon"
version = "0.1.0"
edition.workspace = true
license.workspace = true
rust-version.workspace = true
description = "Canonical Rust binding for Lux Poseidon2 t=2 BN254 (gnark-crypto v0.20.1 compatible). Calls into luxcpp/crypto/poseidon via the C-ABI."
repository = "https://github.com/luxfi/crypto"
readme = "README.md"
keywords = ["poseidon", "poseidon2", "bn254", "zk", "ffi"]
categories = ["cryptography"]
[lib]
name = "lux_crypto_poseidon"
[lints]
workspace = true
+34
View File
@@ -0,0 +1,34 @@
# lux-crypto-poseidon
Canonical Rust binding for **Poseidon2** hash with t=2 over BN254 scalar field.
Wraps the C-ABI exposed by `luxcpp/crypto/poseidon`. Round constants and
permutation match `gnark-crypto` v0.20.1 for byte-for-byte cross-stack
compatibility.
## Algorithm
- **Poseidon2** -- ZK-friendly hash, BN254 scalar field
- Width: t=2 (compression mode)
- gnark-crypto v0.20.1 compatible
## Build
Set `CRYPTO_DIR` (install prefix) or `CRYPTO_BUILD_DIR` (cmake build dir) so
the build script can find `libposeidon_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-poseidon
```
## Attribution
Round constants and permutation derived from `gnark-crypto` v0.20.1
(Apache 2.0).
## License
See `LICENSE` at the repository root.
+48
View File
@@ -0,0 +1,48 @@
// Build script for lux-crypto-poseidon.
//
// Discovers the static archives produced by `luxcpp/crypto/poseidon` and emits
// the link directives. Resolution order (first hit wins):
// 1. CRYPTO_DIR - install prefix; archives at $CRYPTO_DIR/lib/poseidon/
// 2. CRYPTO_BUILD_DIR - cmake build directory; archives at $CRYPTO_BUILD_DIR/poseidon/
// 3. Default fallback to ../../../../luxcpp/crypto/build-cto
//
// We link the C++ runtime (libc++ on macOS, libstdc++ elsewhere) because the
// archive contains C++ object code from the canonical implementation.
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");
let lib_path = base.join("poseidon");
println!("cargo:rustc-link-search=native={}", lib_path.display());
// The umbrella `libposeidon.a` carries the C-ABI shim; the CPU body lives
// in `libposeidon_cpu.a`. Both are required for symbol resolution.
println!("cargo:rustc-link-lib=static=poseidon");
println!("cargo:rustc-link-lib=static=poseidon_cpu");
if cfg!(target_os = "macos") {
println!("cargo:rustc-link-lib=c++");
} else {
println!("cargo:rustc-link-lib=stdc++");
}
}
+56
View File
@@ -0,0 +1,56 @@
// Copyright (c) 2024-2026 Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause-Eco
//
// lux-crypto-poseidon: canonical Rust binding for the Lux Poseidon2 C-ABI.
//
// Links statically against `libposeidon.a` and `libposeidon_cpu.a` produced
// by `luxcpp/crypto/poseidon`, whose CPU body is a first-party port of the
// gnark-crypto v0.20.1 Poseidon2 t=2 BN254 permutation
// (NewParameters(2, 6, 50)) plus the canonical Merkle-Damgard construction
// (NewMerkleDamgardHasher() with 32-byte zero IV).
//
// Byte-equal to upstream gnark-crypto.
#![no_std]
#![forbid(unsafe_op_in_unsafe_fn)]
use core::ffi::c_int;
extern "C" {
/// Poseidon2 t=2 BN254 Merkle-Damgard hash. Returns 0 on success, or a
/// negative `CRYPTO_ERR_*` code on failure.
fn poseidon_bn254(input: *const u8, input_len: usize, output: *mut u8) -> c_int;
}
/// Length of a Poseidon2 BN254 digest in bytes (one fr.Element).
pub const DIGEST_LEN: usize = 32;
/// Compute the Poseidon2 t=2 BN254 Merkle-Damgard hash of `input`.
///
/// `input` is consumed in 32-byte big-endian fr-blocks. If the final block is
/// shorter than 32 bytes, it is left-padded with zeroes (matching the upstream
/// gnark `cloneLeftPadded` semantics). Each block must encode a value strictly
/// less than the BN254 scalar field modulus `r`; otherwise the call returns
/// the empty zero-IV result and reports a non-zero status via the C-ABI return
/// value (a `debug_assert` in this binding catches that condition during
/// development).
#[inline]
pub fn hash(input: &[u8]) -> [u8; DIGEST_LEN] {
let mut out = [0u8; DIGEST_LEN];
// SAFETY: pointers are valid for the duration of the call; output is
// sized to exactly DIGEST_LEN bytes.
let rc = unsafe { poseidon_bn254(input.as_ptr(), input.len(), out.as_mut_ptr()) };
debug_assert_eq!(rc, 0, "poseidon_bn254 returned non-zero status: {}", rc);
out
}
/// Compute the Poseidon2 t=2 BN254 Merkle-Damgard hash of `input` into a
/// caller-supplied buffer. Returns the same byte slice for ergonomic chaining.
#[inline]
pub fn hash_into<'a>(input: &[u8], output: &'a mut [u8; DIGEST_LEN]) -> &'a [u8; DIGEST_LEN] {
// SAFETY: pointers are valid for the duration of the call; output is
// sized to exactly DIGEST_LEN bytes.
let rc = unsafe { poseidon_bn254(input.as_ptr(), input.len(), output.as_mut_ptr()) };
debug_assert_eq!(rc, 0, "poseidon_bn254 returned non-zero status: {}", rc);
output
}
@@ -0,0 +1,143 @@
// Copyright (c) 2024-2026 Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause-Eco
//
// Spec-vector integration test for lux-crypto-poseidon.
//
// All vectors below are unkeyed Poseidon2 Merkle-Damgard hashes produced
// using gnark-crypto v0.20.1
// poseidon2.NewMerkleDamgardHasher()
//
// over the t=2 BN254 permutation
// poseidon2.NewPermutation(2, 6, 50)
//
// The same test suite is also asserted at the C++ layer in
// luxcpp/crypto/poseidon/test/poseidon_test.cpp.
//
// We hand-encode the vectors here (rather than parsing the JSON at runtime)
// so the test stays no-std and independent of any JSON parser.
use lux_crypto_poseidon::{hash, hash_into, DIGEST_LEN};
fn from_hex(s: &str) -> [u8; DIGEST_LEN] {
let s = s.as_bytes();
assert_eq!(s.len(), 2 * DIGEST_LEN, "expected {}-byte hex digest", DIGEST_LEN);
let mut out = [0u8; DIGEST_LEN];
let nibble = |c: u8| -> u8 {
match c {
b'0'..=b'9' => c - b'0',
b'a'..=b'f' => c - b'a' + 10,
b'A'..=b'F' => c - b'A' + 10,
_ => panic!("non-hex byte: {c:#x}"),
}
};
for i in 0..DIGEST_LEN {
out[i] = (nibble(s[2 * i]) << 4) | nibble(s[2 * i + 1]);
}
out
}
fn from_hex_var(s: &str) -> Vec<u8> {
assert!(s.len() % 2 == 0, "hex must be even length");
let nibble = |c: u8| -> u8 {
match c {
b'0'..=b'9' => c - b'0',
b'a'..=b'f' => c - b'a' + 10,
b'A'..=b'F' => c - b'A' + 10,
_ => panic!("non-hex"),
}
};
let b = s.as_bytes();
let mut out = vec![0u8; b.len() / 2];
for i in 0..out.len() {
out[i] = (nibble(b[2 * i]) << 4) | nibble(b[2 * i + 1]);
}
out
}
#[test]
fn hash_32b_zeros() {
let want = from_hex("292c3a4b9343aec63e584aefa8bedeaefae44e6d718451a75736def795109dfb");
assert_eq!(hash(&[0u8; 32]), want);
}
#[test]
fn hash_32b_iota() {
let mut input = [0u8; 32];
for (i, b) in input.iter_mut().enumerate() {
*b = i as u8;
}
let want = from_hex("16db7f49b6261d97cfaa2a8adf9e382c36d2a9d3845612aa7648b6c609946178");
assert_eq!(hash(&input), want);
}
#[test]
fn hash_64b_two_blocks() {
let input = from_hex_var(
"100102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f\
102122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f",
);
let want = from_hex("159a4e06c63599f582b527da70fd1651353de8d82defb38e1daf4e432b6e9f9f");
assert_eq!(hash(&input), want);
}
#[test]
fn hash_96b_three_blocks() {
let input = from_hex_var(
"050102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f\
062122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f\
074142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f",
);
let want = from_hex("16c514c08c2b3397bff233df2188f487609c41942c7da1522b3d7e2347e42a7e");
assert_eq!(hash(&input), want);
}
#[test]
fn hash_5b_short() {
let want = from_hex("1012f1c1aa17396b56597479235b6534e91d7e88026b9bd21e27f42895ae9739");
assert_eq!(hash(b"hello"), want);
}
// The four assertions below come from the same gnark-crypto v0.20.1 KAT
// generator that produces the C++ test's perm/compress vectors: they are the
// single-block hash of an identity permutation input (covering the underlying
// permutation through the Merkle-Damgard front door), plus a handful of
// additional inputs that bracket the BN254 modulus boundary.
#[test]
fn hash_one_byte_zero() {
// 1B input zero-padded on the LEFT to 32 bytes is identical to hashing
// 32 zero bytes (the only nonzero byte of the padded block sits at
// position 31, which is the canonical encoding of fr-element 0x00).
let want = from_hex("292c3a4b9343aec63e584aefa8bedeaefae44e6d718451a75736def795109dfb");
assert_eq!(hash(&[0u8]), want);
}
#[test]
fn hash_31b_zeros_left_pad() {
// 31 zero bytes is < block size, gnark left-pads with one more zero
// -> identical to hash_32b_zeros above.
let want = from_hex("292c3a4b9343aec63e584aefa8bedeaefae44e6d718451a75736def795109dfb");
assert_eq!(hash(&[0u8; 31]), want);
}
#[test]
fn hash_into_is_equivalent_to_hash() {
let mut buf = [0u8; DIGEST_LEN];
hash_into(b"hello", &mut buf);
assert_eq!(buf, hash(b"hello"));
}
#[test]
fn deterministic_across_calls() {
let a = hash(b"deterministic test");
let b = hash(b"deterministic test");
assert_eq!(a, b);
}
#[test]
fn distinct_inputs_distinct_digests() {
// Trivial avalanche check: a 1-bit difference flips many output bits.
let a = hash(b"poseidon-a");
let b = hash(b"poseidon-b");
assert_ne!(a, b);
}
+27
View File
@@ -0,0 +1,27 @@
# lux-crypto-ripemd160
Canonical Rust binding for **RIPEMD-160** (ISO/IEC 10118-3, used in Bitcoin
P2PKH addresses).
Wraps the C-ABI exposed by `luxcpp/crypto/ripemd160`. Verified against the
RIPEMD-160 reference vectors.
## Algorithm
- **RIPEMD-160** -- 20-byte digest, ISO/IEC 10118-3
## Build
Set `CRYPTO_DIR` (install prefix) or `CRYPTO_BUILD_DIR` (cmake build dir) so
the build script can find `libripemd160_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-ripemd160
```
## License
See `LICENSE` at the repository root.
+29
View File
@@ -0,0 +1,29 @@
# lux-crypto-secp256k1
Canonical Rust binding for **secp256k1 ECDSA public-key recovery** (Ethereum-style
`ecrecover`).
Wraps the C-ABI exposed by `luxcpp/crypto/secp256k1`. Single and batch recovery
APIs. Verified against SEC1 and EIP-2 reference vectors in `tests/`.
## Algorithm
- **secp256k1** -- Bitcoin/Ethereum curve (SEC1)
- **ECDSA recovery** -- recover the signer public key from `(msg, r, s, v)`
- Strict `s` low-S enforcement (EIP-2)
## Build
Set `CRYPTO_DIR` (install prefix) or `CRYPTO_BUILD_DIR` (cmake build dir) so the
build script can find `libsecp256k1_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-secp256k1
```
## License
See `LICENSE` at the repository root.
+26
View File
@@ -0,0 +1,26 @@
# lux-crypto-sha256
Canonical Rust binding for **SHA-256** (FIPS 180-4).
Wraps the C-ABI exposed by `luxcpp/crypto/sha256`. Verified against FIPS 180-4
reference vectors.
## Algorithm
- **SHA-256** -- 32-byte digest, FIPS 180-4 standard
## Build
Set `CRYPTO_DIR` (install prefix) or `CRYPTO_BUILD_DIR` (cmake build dir) so
the build script can find `libsha256_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-sha256
```
## License
See `LICENSE` at the repository root.
+32
View File
@@ -0,0 +1,32 @@
# lux-crypto-slhdsa
Canonical Rust binding for **SLH-DSA** (FIPS 205), the NIST-standardized
hash-based stateless post-quantum signature scheme (formerly SPHINCS+).
Wraps the C-ABI exposed by `luxcpp/crypto/slhdsa`. Verified against the NIST
FIPS 205 reference vectors.
## Algorithm
- **SLH-DSA** -- FIPS 205, hash-based, stateless
- Multiple parameter sets (SHA2-128f / SHA2-192f / SHA2-256f and SHAKE variants)
## Build
Set `CRYPTO_DIR` (install prefix) or `CRYPTO_BUILD_DIR` (cmake build dir) so
the build script can find `libslhdsa_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-slhdsa
```
## Attribution
Underlying implementation derived from PQClean (CC0 / public domain).
## License
See `LICENSE` at the repository root.
+31
View File
@@ -0,0 +1,31 @@
# lux-crypto-verkle
Canonical Rust binding for **Verkle** commit and multiproof verification using
the Banderwagon SRS.
Wraps the C-ABI exposed by `luxcpp/crypto/verkle`.
## Algorithm
- **Verkle tree** -- commit, multiproof, verify
- IPA over Banderwagon (see `lux-crypto-ipa`, `lux-crypto-banderwagon`)
## Build
Set `CRYPTO_DIR` (install prefix) or `CRYPTO_BUILD_DIR` (cmake build dir) so
the build script can find `libverkle_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-verkle
```
## Attribution
Banderwagon SRS from the public Verkle ceremony.
## License
See `LICENSE` at the repository root.
+10 -3
View File
@@ -1,10 +1,17 @@
[package]
name = "lux-crypto"
version = "0.1.0"
edition = "2021"
license = "BSD-3-Clause"
rust-version = "1.74"
edition.workspace = true
license.workspace = true
rust-version.workspace = true
authors.workspace = true
homepage.workspace = true
repository.workspace = true
documentation.workspace = true
readme = "README.md"
description = "Umbrella crate for Lux crypto. Re-exports per-algorithm crates that bind to luxcpp/crypto via the C-ABI."
keywords = ["luxfi", "crypto", "blockchain", "post-quantum", "ffi"]
categories = ["cryptography"]
# This crate is the canonical Rust entry point for Lux crypto.
# Mirrors github.com/luxfi/gpu (canonical Rust binding to luxfi/accel).
+57
View File
@@ -0,0 +1,57 @@
# lux-crypto
Umbrella Rust binding to the Lux cryptography library (`luxcpp/crypto`). This
crate re-exports the per-algorithm member crates and retains the original raw
FFI surface for historical callers.
For new code, prefer the per-algorithm crates:
| Crate | Algorithm | Spec |
|-------|-----------|------|
| `lux-crypto-secp256k1` | secp256k1 ECDSA public-key recovery | SEC1, EIP-2 |
| `lux-crypto-keccak` | Keccak-256 | Ethereum Yellow Paper |
| `lux-crypto-sha256` | SHA-256 | FIPS 180-4 |
| `lux-crypto-ripemd160` | RIPEMD-160 | ISO/IEC 10118-3 |
| `lux-crypto-blake2b` | BLAKE2b | RFC 7693 |
| `lux-crypto-blake3` | BLAKE3 | BLAKE3 reference v1.5.0 |
| `lux-crypto-ed25519` | Ed25519 | RFC 8032 |
| `lux-crypto-aead` | ChaCha20-Poly1305 | RFC 8439 |
| `lux-crypto-bls` | BLS12-381 IRTF signatures | draft-irtf-cfrg-bls-signature-05 |
| `lux-crypto-mldsa` | ML-DSA | FIPS 204 |
| `lux-crypto-mlkem` | ML-KEM | FIPS 203 |
| `lux-crypto-slhdsa` | SLH-DSA | FIPS 205 |
| `lux-crypto-lamport` | Lamport OTS | Lamport 1979 |
| `lux-crypto-banderwagon` | Banderwagon group | EIP-7805 |
| `lux-crypto-pedersen` | Pedersen commitments | Verkle |
| `lux-crypto-ipa` | Inner Product Argument | Verkle |
| `lux-crypto-verkle` | Verkle commit + multiproof | Verkle |
| `lux-crypto-evm256` | EVM 256-bit modular arithmetic | EIP-7212 helpers |
| `lux-crypto-kzg` | KZG point-evaluation precompile | EIP-4844 |
| `lux-crypto-poseidon` | Poseidon2 t=2 BN254 | gnark-crypto v0.20.1 |
| `lux-crypto-ntt` | Number-Theoretic Transform | Q = 998244353 |
| `lux-crypto-poly_mul` | Polynomial multiplication in `Z_Q[X]/(X^n+1)` | Q = 998244353 |
## Build
The crate links static archives produced by `luxcpp/crypto`. Set one of:
- `CRYPTO_DIR` -- install prefix; archives at `$CRYPTO_DIR/lib/<alg>/lib<alg>_cpu.a`
- `CRYPTO_BUILD_DIR` -- cmake build directory; archives at `$CRYPTO_BUILD_DIR/<alg>/lib<alg>_cpu.a`
If neither is set the build script falls back to a sibling checkout of
`luxcpp/crypto` at `../../../../luxcpp/crypto/build-cto`.
```bash
# Build the C archives once
git clone https://github.com/luxfi/crypto
cd crypto && cmake -S . -B build-cto && cmake --build build-cto
# Build this crate against them
export CRYPTO_BUILD_DIR=$(pwd)/build-cto
cargo build -p lux-crypto
```
## License
See `LICENSE` at the repository root. Source files declare `SPDX-License-Identifier`
per file; the umbrella project license is the Lux Ecosystem License.