anza-cryptography-source/syscall/bls12-381-syscall/src/encoding.rs
Sam Kim 77b39b268d
[bls12-381-syscall] Add solana-bls12-381-syscall crate (#11)
* add `bls12-381-syscall` crate

* add benchmark check in the CI

* cargo fmt

* remove 16 pair bench since the maximum bound is set to 8
2026-04-22 09:33:15 +09:00

312 lines
10 KiB
Rust

use {
blst::{blst_fp12, blst_lendian_from_fp},
blstrs::{G1Affine, G2Affine, Gt, Scalar},
bytemuck::Zeroable,
bytemuck_derive::{Pod, Zeroable as DeriveZeroable},
};
/// Size of a base field element (`Fq`) in bytes.
pub const FQ_SIZE: usize = 48;
/// Size of a quadratic extension field element (`Fq2`) in bytes.
pub const FQ2_SIZE: usize = 2 * FQ_SIZE;
/// Size of a target group element (`Gt` or `Fq12`) in bytes.
pub const GT_SIZE: usize = 12 * FQ_SIZE;
/// G1 affine point size (uncompressed coordinates).
/// G1 uncompressed = x (48) + y (48) = 96 bytes.
pub const G1_UNCOMPRESSED_SIZE: usize = 2 * FQ_SIZE;
/// G1 compressed point size.
/// G1 compressed = x (48 bytes) with flags encoded in MSB.
pub const G1_COMPRESSED_SIZE: usize = FQ_SIZE;
/// G2 affine point size (uncompressed coordinates).
/// G2 uncompressed = x (96) + y (96) = 192 bytes.
pub const G2_UNCOMPRESSED_SIZE: usize = 2 * FQ2_SIZE;
/// G2 compressed point size.
/// G2 compressed = x (96 bytes) with flags encoded in MSB.
pub const G2_COMPRESSED_SIZE: usize = FQ2_SIZE;
/// Scalar size in bytes.
pub const SCALAR_SIZE: usize = 32;
/// G1 compressed point (48 bytes).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Pod, DeriveZeroable)]
#[repr(transparent)]
pub struct PodG1Compressed(pub [u8; G1_COMPRESSED_SIZE]);
/// G1 affine point (96 bytes).
/// Represents `x` (48 bytes) and `y` (48 bytes).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Pod, DeriveZeroable)]
#[repr(transparent)]
pub struct PodG1Point(pub [u8; G1_UNCOMPRESSED_SIZE]);
impl PodG1Point {
/// Deserializes to an affine point, skipping the subgroup check.
///
/// Checks: Field validity, Curve equation (`y^2 = x^3 + 4`).
/// Skips: Subgroup membership
pub fn to_affine_subgroup_unchecked(&self, endianness: Endianness) -> Option<G1Affine> {
let mut bytes = self.0;
if matches!(endianness, Endianness::LE) {
swap_fq_endianness(&mut bytes);
}
// reject point if the compressed or parity flag is set
if bytes[0] & 0xa0 != 0 {
return None;
}
// `G1Affine::from_uncompressed_unchecked` already performs field and on-curve checks
G1Affine::from_uncompressed_unchecked(&bytes).into_option()
}
/// Deserializes to an affine point with full validation.
///
/// Checks: Field validity, Curve equation (`y^2 = x^3 + 4`), Subgroup membership.
pub fn to_affine(&self, endianness: Endianness) -> Option<G1Affine> {
let affine = self.to_affine_subgroup_unchecked(endianness)?;
if bool::from(affine.is_torsion_free()) {
Some(affine)
} else {
None
}
}
}
/// G2 compressed point (96 bytes).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Pod, DeriveZeroable)]
#[repr(transparent)]
pub struct PodG2Compressed(pub [u8; G2_COMPRESSED_SIZE]);
/// G2 affine point (192 bytes).
/// Represents `x` (96 bytes) and `y` (96 bytes).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Pod, DeriveZeroable)]
#[repr(transparent)]
pub struct PodG2Point(pub [u8; G2_UNCOMPRESSED_SIZE]);
impl PodG2Point {
/// Deserializes to an affine point, skipping the subgroup check.
///
/// Checks: Field validity, Curve equation (`y^2 = x^3 + 4(1+u)^{-1}`).
/// Skips: Subgroup membership
pub fn to_affine_subgroup_unchecked(&self, endianness: Endianness) -> Option<G2Affine> {
let mut bytes = self.0;
if matches!(endianness, Endianness::LE) {
swap_fq_endianness(&mut bytes);
swap_g2_c0_c1(&mut bytes);
}
// reject point if the compressed or parity flag is set
if bytes[0] & 0xa0 != 0 {
return None;
}
// `G2Affine::from_uncompressed_unchecked` already performs field and on-curve checks
G2Affine::from_uncompressed_unchecked(&bytes).into_option()
}
/// Deserializes to an affine point with full validation.
///
/// Checks: Field validity, Curve equation (`y^2 = x^3 + 4(1+u)^{-1}`), Subgroup membership.
pub fn to_affine(&self, endianness: Endianness) -> Option<G2Affine> {
let affine = self.to_affine_subgroup_unchecked(endianness)?;
if bool::from(affine.is_torsion_free()) {
Some(affine)
} else {
None
}
}
}
/// Scalar value (32 bytes).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Pod, DeriveZeroable)]
#[repr(transparent)]
pub struct PodScalar(pub [u8; SCALAR_SIZE]);
impl PodScalar {
pub fn to_scalar(&self, endianness: Endianness) -> Option<Scalar> {
match endianness {
Endianness::BE => Scalar::from_bytes_be(&self.0).into_option(),
Endianness::LE => Scalar::from_bytes_le(&self.0).into_option(),
}
}
}
/// Target group element (Gt).
/// Represents an element in the extension field Fq12 (576 bytes).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Pod, DeriveZeroable)]
#[repr(transparent)]
pub struct PodGtElement(pub [u8; GT_SIZE]);
/// Specifies the byte ordering for BLS12-381 field elements.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Endianness {
/// Canonical Big-Endian encoding, following the [Zcash][zcash] and [IETF][ietf]
/// standards.
///
/// [zcash]: https://github.com/zkcrypto/pairing/tree/master/src/bls12_381#serialization
/// [ietf]: https://www.ietf.org/archive/id/draft-irtf-cfrg-pairing-friendly-curves-11.html#name-bls-curves-for-the-128-bit-
BE,
/// Little-Endian encoding.
/// Base field elements (`Fq`) are reversed compared to BE.
LE,
}
/// Toggles the endianness of `Fq` (base field) elements in place.
///
/// Since a BLS12-381 `Fq` element is represented by 48 bytes, this function
/// iterates through the input buffer in 48-byte chunks and reverses the byte
/// order of each chunk.
pub(crate) fn swap_fq_endianness(bytes: &mut [u8]) {
for chunk in bytes.chunks_mut(FQ_SIZE) {
chunk.reverse();
}
}
/// Swaps the real (`c_0`) and imaginary (`c_1`) components of `Fq2` elements in place.
///
/// In the Zcash/IETF Big-Endian standard, `Fq2` elements are ordered as `c1` then `c0`.
/// In the Little-Endian variant, they are ordered as `c0` then `c1` to match
/// standard polynomial representation ($c_0 + c_1 u$).
///
/// This function expects 96-byte chunks (representing one `Fq2` element or a G2 coordinate)
/// and swaps the first 48 bytes with the last 48 bytes.
pub(crate) fn swap_g2_c0_c1(bytes: &mut [u8]) {
for fq2_chunk in bytes.chunks_exact_mut(FQ2_SIZE) {
let (c0, c1) = fq2_chunk.split_at_mut(FQ_SIZE);
c0.swap_with_slice(c1);
}
}
/// Helper to serialize Fp12 (Gt) according to SIMD Endianness rules.
///
/// `Fp12 = c0(Fp6) + c1(Fp6)w`.
/// `Fp6 = c0(Fp2) + c1(Fp2)v + c2(Fp2)v^2`.
/// `Fp2 = c0(Fp) + c1(Fp)u`.
///
/// # Rules
/// 1. **Fq (`Fp`)**:
/// - BE: Big-Endian bytes.
/// - LE: Little-Endian bytes.
/// 2. **Coefficient Ordering**:
/// - BE: Highest degree coefficients first (reverses the entire tower).
/// - LE: Lowest degree coefficients first (canonical memory order).
pub(crate) fn serialize_gt(gt: Gt, endianness: Endianness) -> PodGtElement {
// `blstrs::Gt` is `repr(transparent)` over `blst_fp12`.
// We transmute to access the internal coefficients directly because
// blstrs does not expose the `Fp12`/`Fp6`/`Fp2`/`Fp` types publicly.
// Safe because layout is guaranteed by `repr(transparent)` wrapping the blst type.
// We also verify in `test_gt_layout_safety` that layout sizes and alignment match.
let val: blst_fp12 = unsafe { std::mem::transmute(gt) };
let mut out = PodGtElement::zeroed();
let mut ptr = out.0.as_mut_ptr();
unsafe {
for fp6 in val.fp6.iter() {
for fp2 in fp6.fp2.iter() {
// In LE, we write c0 (Real) then c1 (Imaginary)
let c0 = &fp2.fp[0];
let c1 = &fp2.fp[1];
blst_lendian_from_fp(ptr, c0);
ptr = ptr.add(FQ_SIZE);
blst_lendian_from_fp(ptr, c1);
ptr = ptr.add(FQ_SIZE);
}
}
}
// If Big Endian is requested, simply reverse the entire byte array, which
// achieves both of the following simultaneously:
// - flips coefficient order (c0...c11 -> c11...c0)
// - flips coefficient bytes (LE -> BE)
if matches!(endianness, Endianness::BE) {
out.0.reverse();
}
out
}
#[cfg(test)]
mod tests {
use {super::*, blstrs::Gt, group::Group};
#[test]
fn test_swap_fq_endianness() {
// Create a buffer with 2 chunks (96 bytes)
// Chunk 0: 0..48
// Chunk 1: 48..96
let mut input = Vec::new();
for i in 0..96u8 {
input.push(i);
}
let mut expected = input.clone();
expected[0..48].reverse();
expected[48..96].reverse();
swap_fq_endianness(&mut input);
assert_eq!(input, expected);
}
#[test]
fn test_swap_g2_c0_c1() {
// Create a buffer with 1 G2 point (96 bytes)
// c0: 48 bytes of 0xAA
// c1: 48 bytes of 0xBB
let mut input = Vec::new();
input.extend_from_slice(&[0xAAu8; 48]);
input.extend_from_slice(&[0xBBu8; 48]);
// Expected result: c1 then c0
let mut expected = Vec::new();
expected.extend_from_slice(&[0xBBu8; 48]);
expected.extend_from_slice(&[0xAAu8; 48]);
swap_g2_c0_c1(&mut input);
assert_eq!(input, expected);
}
#[test]
fn test_serialize_gt_identity() {
let identity = Gt::identity();
// BE
let be_bytes = serialize_gt(identity, Endianness::BE);
assert_eq!(&be_bytes.0[0..48], &[0u8; 48]);
let mut one_be = [0u8; 48];
one_be[47] = 1;
assert_eq!(&be_bytes.0[528..576], &one_be);
// LE
let le_bytes = serialize_gt(identity, Endianness::LE);
let mut one_le = [0u8; 48];
one_le[0] = 1; // [01, 00, ... 00]
assert_eq!(&le_bytes.0[0..48], &one_le);
assert_eq!(&le_bytes.0[48..96], &[0u8; 48]);
}
#[test]
fn test_gt_layout_safety() {
// Critical Safety Check:
// verify that blstrs::Gt is indeed a transparent wrapper around blst_fp12
// before we rely on unsafe transmutes in the main code.
assert_eq!(
std::mem::size_of::<Gt>(),
std::mem::size_of::<blst_fp12>(),
"Size mismatch between blstrs::Gt and blst_fp12. Unsafe transmute is invalid."
);
assert_eq!(
std::mem::align_of::<Gt>(),
std::mem::align_of::<blst_fp12>(),
"Alignment mismatch between blstrs::Gt and blst_fp12. Unsafe transmute is invalid."
);
}
}