Skip to content

Commit 13263e8

Browse files
authored
Implement BLS12-381 point negation (#1456)
### What Split out point negation part ([0945897](0945897)) from #1449, and address review comments on this part. ### Why Improves usability of the BLS12-381 features in a Groth16 verifier application ([example contract](stellar/soroban-examples#350)) - `Neg` is an common operation, needed for using the proof parameter as pairing input, and is cheap and simple enough to be implemented as an sdk function. ### Known limitations [TODO or N/A]
1 parent f8bec23 commit 13263e8

3 files changed

Lines changed: 312 additions & 22 deletions

File tree

soroban-sdk/src/bytes.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ macro_rules! bytesn {
104104

105105
#[macro_export]
106106
macro_rules! impl_bytesn_repr {
107-
($elem: ident, $size: literal) => {
107+
($elem: ident, $size: expr) => {
108108
impl $elem {
109109
pub fn from_bytes(bytes: BytesN<$size>) -> Self {
110110
Self(bytes)

soroban-sdk/src/crypto/bls12_381.rs

Lines changed: 250 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,79 @@ use crate::{
77
use core::{
88
cmp::Ordering,
99
fmt::Debug,
10-
ops::{Add, Mul, Sub},
10+
ops::{Add, Mul, Neg, Sub},
1111
};
1212

13+
pub const FP_SERIALIZED_SIZE: usize = 48; // Size in bytes of a serialized Fp element in BLS12-381. The field modulus is 381 bits, requiring 48 bytes (384 bits) with 3 bits reserved for flags.
14+
pub const FP2_SERIALIZED_SIZE: usize = FP_SERIALIZED_SIZE * 2;
15+
pub const G1_SERIALIZED_SIZE: usize = FP_SERIALIZED_SIZE * 2;
16+
pub const G2_SERIALIZED_SIZE: usize = FP2_SERIALIZED_SIZE * 2;
17+
1318
/// Bls12_381 provides access to curve and field arithmetics on the BLS12-381
1419
/// curve.
1520
pub struct Bls12_381 {
1621
env: Env,
1722
}
1823

24+
// This routine was copied with slight modification from the arkworks library:
25+
// https://github.com/arkworks-rs/algebra/blob/bf1c9b22b30325ef4df4f701dedcb6dea904c587/ff/src/biginteger/arithmetic.rs#L66-L79
26+
fn sbb_for_sub_with_borrow(a: &mut u64, b: u64, borrow: u8) -> u8 {
27+
let tmp = (1u128 << 64) + u128::from(*a) - u128::from(b) - u128::from(borrow);
28+
// casting is safe here because `tmp` can only exceed u64 by a single
29+
// (borrow) bit, which we capture in the next line.
30+
*a = tmp as u64;
31+
u8::from(tmp >> 64 == 0)
32+
}
33+
34+
#[derive(Debug)]
35+
pub(crate) struct BigInt<const N: usize>(pub [u64; N]);
36+
37+
impl<const N: usize> BigInt<N> {
38+
pub fn sub_with_borrow(&mut self, other: &Self) -> bool {
39+
let mut borrow = 0;
40+
for i in 0..N {
41+
borrow = sbb_for_sub_with_borrow(&mut self.0[i], other.0[i], borrow);
42+
}
43+
borrow != 0
44+
}
45+
46+
pub fn copy_into_array<const M: usize>(&self, slice: &mut [u8; M]) {
47+
const {
48+
if M != N * 8 {
49+
panic!("BigInt::copy_into_array with mismatched array length")
50+
}
51+
}
52+
53+
for i in 0..N {
54+
let limb_bytes = self.0[N - 1 - i].to_be_bytes();
55+
slice[i * 8..(i + 1) * 8].copy_from_slice(&limb_bytes);
56+
}
57+
}
58+
59+
pub fn is_zero(&self) -> bool {
60+
self.0 == [0; N]
61+
}
62+
}
63+
64+
impl<const N: usize, const M: usize> From<&BytesN<M>> for BigInt<N> {
65+
fn from(bytes: &BytesN<M>) -> Self {
66+
if M != N * 8 {
67+
panic!("BytesN::Into<BigInt> - length mismatch")
68+
}
69+
70+
let array = bytes.to_array();
71+
let mut limbs = [0u64; N];
72+
for i in 0..N {
73+
let start = i * 8;
74+
let end = start + 8;
75+
let mut chunk = [0u8; 8];
76+
chunk.copy_from_slice(&array[start..end]);
77+
limbs[N - 1 - i] = u64::from_be_bytes(chunk);
78+
}
79+
BigInt(limbs)
80+
}
81+
}
82+
1983
/// `G1Affine` is a point in the G1 group (subgroup defined over the base field
2084
/// `Fq`) of the BLS12-381 elliptic curve
2185
///
@@ -44,7 +108,7 @@ pub struct Bls12_381 {
44108
/// ```
45109
#[derive(Clone)]
46110
#[repr(transparent)]
47-
pub struct G1Affine(BytesN<96>);
111+
pub struct G1Affine(BytesN<G1_SERIALIZED_SIZE>);
48112

49113
/// `G2Affine` is a point in the G2 group (subgroup defined over the quadratic
50114
/// extension field `Fq2`) of the BLS12-381 elliptic curve
@@ -64,7 +128,7 @@ pub struct G1Affine(BytesN<96>);
64128
/// - sort_flag (bit 2): Must always be unset (0).
65129
#[derive(Clone)]
66130
#[repr(transparent)]
67-
pub struct G2Affine(BytesN<192>);
131+
pub struct G2Affine(BytesN<G2_SERIALIZED_SIZE>);
68132

69133
/// `Fp` represents an element of the base field `Fq` of the BLS12-381 elliptic
70134
/// curve
@@ -74,7 +138,7 @@ pub struct G2Affine(BytesN<192>);
74138
/// field `Fp`. The value is serialized as a big-endian integer.
75139
#[derive(Clone)]
76140
#[repr(transparent)]
77-
pub struct Fp(BytesN<48>);
141+
pub struct Fp(BytesN<FP_SERIALIZED_SIZE>);
78142

79143
/// `Fp2` represents an element of the quadratic extension field `Fq2` of the
80144
/// BLS12-381 elliptic curve
@@ -86,7 +150,7 @@ pub struct Fp(BytesN<48>);
86150
/// and imaginary components).
87151
#[derive(Clone)]
88152
#[repr(transparent)]
89-
pub struct Fp2(BytesN<96>);
153+
pub struct Fp2(BytesN<FP2_SERIALIZED_SIZE>);
90154

91155
/// `Fr` represents an element in the BLS12-381 scalar field, which is a prime
92156
/// field of order `r` (the order of the G1 and G2 groups). The struct is
@@ -96,10 +160,88 @@ pub struct Fp2(BytesN<96>);
96160
#[repr(transparent)]
97161
pub struct Fr(U256);
98162

99-
impl_bytesn_repr!(G1Affine, 96);
100-
impl_bytesn_repr!(G2Affine, 192);
101-
impl_bytesn_repr!(Fp, 48);
102-
impl_bytesn_repr!(Fp2, 96);
163+
impl_bytesn_repr!(G1Affine, G1_SERIALIZED_SIZE);
164+
impl_bytesn_repr!(G2Affine, G2_SERIALIZED_SIZE);
165+
impl_bytesn_repr!(Fp, FP_SERIALIZED_SIZE);
166+
impl_bytesn_repr!(Fp2, FP2_SERIALIZED_SIZE);
167+
168+
impl Fp {
169+
pub fn env(&self) -> &Env {
170+
self.0.env()
171+
}
172+
173+
// `Fp` represents an element in the base field of the BLS12-381 elliptic curve.
174+
// For an element a ∈ Fp, its negation `-a` is defined as:
175+
// a + (-a) = 0 (mod p)
176+
// where `p` is the field modulus, and to make a valid point coordinate on the
177+
// curve, `a` also must be within the field range (i.e., 0 ≤ a < p).
178+
fn checked_neg(&self) -> Option<Fp> {
179+
let fp_bigint: BigInt<6> = (&self.0).into();
180+
if fp_bigint.is_zero() {
181+
return Some(self.clone());
182+
}
183+
184+
// BLS12-381 base field modulus
185+
const BLS12_381_MODULUS: [u64; 6] = [
186+
13402431016077863595,
187+
2210141511517208575,
188+
7435674573564081700,
189+
7239337960414712511,
190+
5412103778470702295,
191+
1873798617647539866,
192+
];
193+
let mut res = BigInt(BLS12_381_MODULUS);
194+
195+
// Compute modulus - value
196+
let borrow = res.sub_with_borrow(&fp_bigint);
197+
if borrow {
198+
return None;
199+
}
200+
201+
let mut bytes = [0u8; FP_SERIALIZED_SIZE];
202+
res.copy_into_array(&mut bytes);
203+
Some(Fp::from_array(self.env(), &bytes))
204+
}
205+
206+
/// Maps to a `G1Affine` point via [simplified SWU
207+
/// mapping](https://www.rfc-editor.org/rfc/rfc9380.html#name-simplified-swu-for-ab-0)
208+
pub fn map_to_g1(&self) -> G1Affine {
209+
self.env().crypto().bls12_381().map_fp_to_g1(self)
210+
}
211+
}
212+
213+
impl From<Fp> for BigInt<6> {
214+
fn from(fp: Fp) -> Self {
215+
let inner: Bytes = fp.0.into();
216+
let mut limbs = [0u64; 6];
217+
for i in 0..6u32 {
218+
let start = i * 8;
219+
let mut slice = [0u8; 8];
220+
inner.slice(start..start + 8).copy_into_slice(&mut slice);
221+
limbs[5 - i as usize] = u64::from_be_bytes(slice);
222+
}
223+
BigInt(limbs)
224+
}
225+
}
226+
227+
impl Neg for &Fp {
228+
type Output = Fp;
229+
230+
fn neg(self) -> Self::Output {
231+
match self.checked_neg() {
232+
Some(v) => v,
233+
None => sdk_panic!("invalid input - Fp is larger than the field modulus"),
234+
}
235+
}
236+
}
237+
238+
impl Neg for Fp {
239+
type Output = Fp;
240+
241+
fn neg(self) -> Self::Output {
242+
(&self).neg()
243+
}
244+
}
103245

104246
impl G1Affine {
105247
pub fn env(&self) -> &Env {
@@ -131,6 +273,87 @@ impl Mul<Fr> for G1Affine {
131273
}
132274
}
133275

276+
// G1Affine represents a point (X, Y) on the BLS12-381 curve where X, Y ∈ Fp
277+
// Negation of (X, Y) is defined as (X, -Y)
278+
impl Neg for &G1Affine {
279+
type Output = G1Affine;
280+
281+
fn neg(self) -> Self::Output {
282+
let mut inner: Bytes = (&self.0).into();
283+
let y = Fp::try_from_val(
284+
inner.env(),
285+
inner.slice(FP_SERIALIZED_SIZE as u32..).as_val(),
286+
)
287+
.unwrap_optimized();
288+
let neg_y = -y;
289+
inner.copy_from_slice(FP_SERIALIZED_SIZE as u32, &neg_y.to_array());
290+
G1Affine::from_bytes(BytesN::try_from_val(inner.env(), inner.as_val()).unwrap_optimized())
291+
}
292+
}
293+
294+
impl Neg for G1Affine {
295+
type Output = G1Affine;
296+
297+
fn neg(self) -> Self::Output {
298+
(&self).neg()
299+
}
300+
}
301+
302+
impl Fp2 {
303+
pub fn env(&self) -> &Env {
304+
self.0.env()
305+
}
306+
307+
// An Fp2 element is represented as c0 + c1 * X, where:
308+
// - c0, c1 are base field elements (Fp)
309+
// - X is the quadratic non-residue used to construct the field extension
310+
// The negation of c0 + c1 * X is (-c0) + (-c1) * X.
311+
fn checked_neg(&self) -> Option<Fp2> {
312+
let mut inner = self.to_array();
313+
let mut slice0 = [0; FP_SERIALIZED_SIZE];
314+
let mut slice1 = [0; FP_SERIALIZED_SIZE];
315+
slice0.copy_from_slice(&inner[0..FP_SERIALIZED_SIZE]);
316+
slice1.copy_from_slice(&inner[FP_SERIALIZED_SIZE..FP2_SERIALIZED_SIZE]);
317+
318+
// Convert both components to Fp and negate them
319+
let c0 = Fp::from_array(self.env(), &slice0);
320+
let c1 = Fp::from_array(self.env(), &slice1);
321+
322+
// If either component's negation fails, the whole operation fails
323+
let neg_c0 = c0.checked_neg()?;
324+
let neg_c1 = c1.checked_neg()?;
325+
326+
// Reconstruct the Fp2 element from negated components
327+
inner[0..FP_SERIALIZED_SIZE].copy_from_slice(&neg_c0.to_array());
328+
inner[FP_SERIALIZED_SIZE..FP2_SERIALIZED_SIZE].copy_from_slice(&neg_c1.to_array());
329+
330+
Some(Fp2::from_array(self.env(), &inner))
331+
}
332+
333+
pub fn map_to_g2(&self) -> G2Affine {
334+
self.env().crypto().bls12_381().map_fp2_to_g2(self)
335+
}
336+
}
337+
338+
impl Neg for &Fp2 {
339+
type Output = Fp2;
340+
341+
fn neg(self) -> Self::Output {
342+
match self.checked_neg() {
343+
Some(v) => v,
344+
None => sdk_panic!("invalid input - Fp2 component is larger than the field modulus"),
345+
}
346+
}
347+
}
348+
349+
impl Neg for Fp2 {
350+
type Output = Fp2;
351+
352+
fn neg(self) -> Self::Output {
353+
(&self).neg()
354+
}
355+
}
356+
134357
impl G2Affine {
135358
pub fn env(&self) -> &Env {
136359
self.0.env()
@@ -161,23 +384,29 @@ impl Mul<Fr> for G2Affine {
161384
}
162385
}
163386

164-
impl Fp {
165-
pub fn env(&self) -> &Env {
166-
self.0.env()
167-
}
387+
// G2Affine represents a point (X, Y) on the BLS12-381 quadratic extension curve where X, Y ∈ Fp2
388+
// Negation of (X, Y) is defined as (X, -Y)
389+
impl Neg for &G2Affine {
390+
type Output = G2Affine;
168391

169-
pub fn map_to_g1(&self) -> G1Affine {
170-
self.env().crypto().bls12_381().map_fp_to_g1(self)
392+
fn neg(self) -> Self::Output {
393+
let mut inner: Bytes = (&self.0).into();
394+
let y = Fp2::try_from_val(
395+
inner.env(),
396+
inner.slice(FP2_SERIALIZED_SIZE as u32..).as_val(),
397+
)
398+
.unwrap_optimized();
399+
let neg_y = -y;
400+
inner.copy_from_slice(FP2_SERIALIZED_SIZE as u32, &neg_y.to_array());
401+
G2Affine::from_bytes(BytesN::try_from_val(inner.env(), inner.as_val()).unwrap_optimized())
171402
}
172403
}
173404

174-
impl Fp2 {
175-
pub fn env(&self) -> &Env {
176-
self.0.env()
177-
}
405+
impl Neg for G2Affine {
406+
type Output = G2Affine;
178407

179-
pub fn map_to_g2(&self) -> G2Affine {
180-
self.env().crypto().bls12_381().map_fp2_to_g2(self)
408+
fn neg(self) -> Self::Output {
409+
(&self).neg()
181410
}
182411
}
183412

0 commit comments

Comments
 (0)