Skip to main content

airbender_crypto/bls12_381/
eip2537.rs

1// EIP-2537 helpers for BLS12-381 precompiles.
2// These are defined in the crypto crate to avoid ICE when compiling for RISC-V.
3// The issue is that functions in external crates that use Fq/G1/G2 types trigger
4// compiler bugs during predicate checking for generic constants.
5
6use super::curves::{g1, g2};
7use super::{Fq, Fq2, G1Affine, G2Affine};
8use crate::ark_ec::hashing::curve_maps::swu::SWUMap;
9use crate::ark_ec::hashing::curve_maps::wb::{IsogenyMap, WBConfig};
10use crate::ark_ec::hashing::map_to_curve_hasher::MapToCurve;
11use crate::ark_ec::hashing::HashToCurveError;
12use crate::ark_ec::models::short_weierstrass::SWCurveConfig;
13use crate::ark_ec::short_weierstrass::Affine;
14use crate::ark_ec::AffineRepr;
15use crate::ark_ff::{AdditiveGroup, Field, PrimeField};
16
17const FIELD_ELEMENT_LEN: usize = 64;
18const G1_LEN: usize = 128;
19const G2_LEN: usize = 256;
20
21#[inline(never)]
22pub fn parse_fq_bytes(input: &[u8; FIELD_ELEMENT_LEN]) -> Option<Fq> {
23    if input[..16].iter().all(|el| *el == 0) == false {
24        return None;
25    }
26    let mut repr = <Fq as PrimeField>::BigInt::zero();
27    let repr_slice = repr.as_mut();
28    for (dst, src) in repr_slice.iter_mut().zip(input[16..].chunks_exact(8).rev()) {
29        *dst = u64::from_be_bytes(src.try_into().unwrap());
30    }
31    Fq::from_bigint(repr)
32}
33
34#[inline(never)]
35pub fn parse_fq2_bytes(input: &[u8; FIELD_ELEMENT_LEN * 2]) -> Option<Fq2> {
36    let c0 = parse_fq_bytes(input[0..64].try_into().ok()?)?;
37    let c1 = parse_fq_bytes(input[64..128].try_into().ok()?)?;
38    Some(Fq2 { c0, c1 })
39}
40
41#[inline(never)]
42pub fn parse_g1_bytes(input: &[u8; G1_LEN]) -> Option<(G1Affine, bool)> {
43    if input.iter().all(|el| *el == 0) {
44        return Some((G1Affine::identity(), false));
45    }
46    let x = parse_fq_bytes(input[0..64].try_into().ok()?)?;
47    let y = parse_fq_bytes(input[64..128].try_into().ok()?)?;
48    let point = G1Affine::new_unchecked(x, y);
49
50    if !point.is_on_curve() {
51        return None;
52    }
53
54    Some((point, true))
55}
56
57#[inline(never)]
58pub fn parse_g2_bytes(input: &[u8; G2_LEN]) -> Option<(G2Affine, bool)> {
59    if input.iter().all(|el| *el == 0) {
60        return Some((G2Affine::identity(), false));
61    }
62    let x = parse_fq2_bytes(input[0..128].try_into().ok()?)?;
63    let y = parse_fq2_bytes(input[128..256].try_into().ok()?)?;
64    let point = G2Affine::new_unchecked(x, y);
65
66    if !point.is_on_curve() {
67        return None;
68    }
69
70    Some((point, true))
71}
72
73#[inline(never)]
74pub fn serialize_fq_bytes(el: Fq, output: &mut [u8; FIELD_ELEMENT_LEN]) {
75    output[..16].fill(0);
76    let bigint = el.into_bigint();
77    let words = bigint.as_ref();
78    for (i, word) in words.iter().take(6).enumerate() {
79        let bytes = word.to_be_bytes();
80        let start = 16 + (5 - i) * 8;
81        output[start..start + 8].copy_from_slice(&bytes);
82    }
83}
84
85#[inline(never)]
86pub fn serialize_fq2_bytes(el: Fq2, output: &mut [u8; FIELD_ELEMENT_LEN * 2]) {
87    let (left, right) = output.split_at_mut(64);
88    serialize_fq_bytes(el.c0, left.try_into().unwrap());
89    serialize_fq_bytes(el.c1, right.try_into().unwrap());
90}
91
92#[inline(never)]
93pub fn serialize_g1_bytes(el: G1Affine, output: &mut [u8; G1_LEN]) {
94    if let Some((x, y)) = el.xy() {
95        let (left, right) = output.split_at_mut(64);
96        serialize_fq_bytes(x, left.try_into().unwrap());
97        serialize_fq_bytes(y, right.try_into().unwrap());
98    } else {
99        output.fill(0);
100    }
101}
102
103#[inline(never)]
104pub fn serialize_g2_bytes(el: G2Affine, output: &mut [u8; G2_LEN]) {
105    if let Some((x, y)) = el.xy() {
106        let (left, right) = output.split_at_mut(128);
107        serialize_fq2_bytes(x, left.try_into().unwrap());
108        serialize_fq2_bytes(y, right.try_into().unwrap());
109    } else {
110        output.fill(0);
111    }
112}
113
114// Heap-free reimplementation of arkworks' IsogenyMap::apply + polynomial evaluation.
115// Original: https://github.com/arkworks-rs/algebra/blob/af564e48/ec/src/hashing/curve_maps/wb.rs#L42-L64
116fn evaluate_polynomial<F: Field>(coeffs: &[F], x: &F) -> F {
117    if coeffs.is_empty() {
118        return F::ZERO;
119    }
120    if x.is_zero() {
121        return coeffs[0];
122    }
123    coeffs
124        .iter()
125        .rfold(F::ZERO, |result, coeff| result * x + coeff)
126}
127
128// Heap-free `IsogenyMap::apply` using Horner evaluation + Montgomery's trick.
129fn apply_isogeny_map<
130    Domain: SWCurveConfig,
131    Codomain: SWCurveConfig<BaseField = Domain::BaseField>,
132>(
133    map: &IsogenyMap<'_, Domain, Codomain>,
134    domain_point: Affine<Domain>,
135) -> Result<Affine<Codomain>, HashToCurveError> {
136    match domain_point.xy() {
137        Some((x, y)) => {
138            let x_num = evaluate_polynomial(map.x_map_numerator, &x);
139            let x_den = evaluate_polynomial(map.x_map_denominator, &x);
140            let y_num = evaluate_polynomial(map.y_map_numerator, &x);
141            let y_den = evaluate_polynomial(map.y_map_denominator, &x);
142
143            let zero = Domain::BaseField::ZERO;
144            let prod = x_den * y_den;
145            let (x_den_inv, y_den_inv) = if let Some(prod_inv) = prod.inverse() {
146                (y_den * prod_inv, x_den * prod_inv)
147            } else {
148                (
149                    x_den.inverse().unwrap_or(zero),
150                    y_den.inverse().unwrap_or(zero),
151                )
152            };
153            let img_x = x_num * x_den_inv;
154            let img_y = (y_num * y) * y_den_inv;
155            Ok(Affine::<Codomain>::new_unchecked(img_x, img_y))
156        }
157        None => Ok(Affine::identity()),
158    }
159}
160
161#[inline(never)]
162pub fn map_fp_to_g1(element: Fq) -> Result<G1Affine, HashToCurveError> {
163    let point_on_iso_curve =
164        SWUMap::<<g1::Config as WBConfig>::IsogenousCurve>::map_to_curve(element)?;
165    apply_isogeny_map(&g1::Config::ISOGENY_MAP, point_on_iso_curve)
166}
167
168#[inline(never)]
169pub fn map_fp2_to_g2(element: Fq2) -> Result<G2Affine, HashToCurveError> {
170    let point_on_iso_curve =
171        SWUMap::<<g2::Config as WBConfig>::IsogenousCurve>::map_to_curve(element)?;
172    apply_isogeny_map(&g2::Config::ISOGENY_MAP, point_on_iso_curve)
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178    use crate::ark_ec::hashing::curve_maps::wb::WBMap;
179    use proptest::{prop_assert_eq, proptest};
180
181    #[test]
182    fn map_fp_to_g1_matches_arkworks() {
183        proptest!(|(bytes: [u8; 48])| {
184            let mut repr = <Fq as PrimeField>::BigInt::zero();
185            for (dst, src) in repr.as_mut().iter_mut().zip(bytes.chunks_exact(8)) {
186                *dst = u64::from_le_bytes(src.try_into().unwrap());
187            }
188            if let Some(element) = Fq::from_bigint(repr) {
189                let ours = map_fp_to_g1(element).unwrap();
190                let reference = WBMap::<g1::Config>::map_to_curve(element).unwrap();
191                prop_assert_eq!(ours, reference);
192            }
193        })
194    }
195
196    #[test]
197    fn apply_isogeny_map_zero_denominator() {
198        // Craft an isogeny map where x_den is constant zero.
199        // Montgomery's trick: prod = 0 * y_den = 0 → fallback path.
200        let one = Fq::ONE;
201        let zero = Fq::ZERO;
202
203        // Use g1::Config as both Domain and Codomain (same BaseField).
204        let isogeny = IsogenyMap::<g1::Config, g1::Config> {
205            x_map_numerator: &[one],
206            x_map_denominator: &[zero], // always evaluates to zero
207            y_map_numerator: &[one],
208            y_map_denominator: &[one],
209        };
210
211        let input = G1Affine::generator();
212        let result = apply_isogeny_map(&isogeny, input).unwrap();
213        // x_den=0 → x_den_inv=0 → img_x=0, but y_den=1 → img_y preserved
214        assert_eq!(result.x, Fq::ZERO);
215        assert_ne!(result.y, Fq::ZERO);
216
217        // Both denominators zero
218        let isogeny_both_zero = IsogenyMap::<g1::Config, g1::Config> {
219            x_map_numerator: &[one],
220            x_map_denominator: &[zero],
221            y_map_numerator: &[one],
222            y_map_denominator: &[zero],
223        };
224        let result = apply_isogeny_map(&isogeny_both_zero, input).unwrap();
225        assert_eq!(result.x, Fq::ZERO);
226        assert_eq!(result.y, Fq::ZERO);
227    }
228
229    #[test]
230    fn map_fp2_to_g2_matches_arkworks() {
231        proptest!(|(bytes: [u8; 96])| {
232            let mut repr0 = <Fq as PrimeField>::BigInt::zero();
233            let mut repr1 = <Fq as PrimeField>::BigInt::zero();
234            for (dst, src) in repr0.as_mut().iter_mut().zip(bytes[..48].chunks_exact(8)) {
235                *dst = u64::from_le_bytes(src.try_into().unwrap());
236            }
237            for (dst, src) in repr1.as_mut().iter_mut().zip(bytes[48..].chunks_exact(8)) {
238                *dst = u64::from_le_bytes(src.try_into().unwrap());
239            }
240            if let (Some(c0), Some(c1)) = (Fq::from_bigint(repr0), Fq::from_bigint(repr1)) {
241                let element = Fq2 { c0, c1 };
242                let ours = map_fp2_to_g2(element).unwrap();
243                let reference = WBMap::<g2::Config>::map_to_curve(element).unwrap();
244                prop_assert_eq!(ours, reference);
245            }
246        })
247    }
248}