Add implementation of point addition for projective group elements.
This commit is contained in:
parent
4678a67ce6
commit
f996747fd6
|
@ -35,6 +35,9 @@ fn criterion_benchmark(c: &mut Criterion) {
|
|||
c.bench_function(&format!("{} doubling", name), move |b| {
|
||||
b.iter(|| black_box(a).double())
|
||||
});
|
||||
c.bench_function(&format!("{} addition", name), move |b| {
|
||||
b.iter(|| black_box(a).add(&a))
|
||||
});
|
||||
}
|
||||
|
||||
// G2Affine
|
||||
|
@ -65,6 +68,9 @@ fn criterion_benchmark(c: &mut Criterion) {
|
|||
c.bench_function(&format!("{} doubling", name), move |b| {
|
||||
b.iter(|| black_box(a).double())
|
||||
});
|
||||
c.bench_function(&format!("{} addition", name), move |b| {
|
||||
b.iter(|| black_box(a).add(&a))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
244
src/g1.rs
244
src/g1.rs
|
@ -1,8 +1,11 @@
|
|||
//! This module provides an implementation of the $\mathbb{G}_1$ group of BLS12-381.
|
||||
|
||||
use crate::fp::Fp;
|
||||
use core::ops::{Add, AddAssign, Neg, Sub, SubAssign};
|
||||
|
||||
use subtle::{Choice, ConditionallySelectable, ConstantTimeEq};
|
||||
|
||||
use crate::fp::Fp;
|
||||
|
||||
/// This is an element of $\mathbb{G}_1$ represented in the affine coordinate space.
|
||||
/// It is ideal to keep elements in this representation to reduce memory usage and
|
||||
/// improve performance through the use of mixed curve model arithmetic.
|
||||
|
@ -192,6 +195,48 @@ impl PartialEq for G1Projective {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a> Neg for &'a G1Projective {
|
||||
type Output = G1Projective;
|
||||
|
||||
#[inline]
|
||||
fn neg(self) -> G1Projective {
|
||||
G1Projective {
|
||||
x: self.x,
|
||||
y: -self.y,
|
||||
z: self.z,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Neg for G1Projective {
|
||||
type Output = G1Projective;
|
||||
|
||||
#[inline]
|
||||
fn neg(self) -> G1Projective {
|
||||
-&self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> Add<&'b G1Projective> for &'a G1Projective {
|
||||
type Output = G1Projective;
|
||||
|
||||
#[inline]
|
||||
fn add(self, rhs: &'b G1Projective) -> G1Projective {
|
||||
self.add(rhs)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> Sub<&'b G1Projective> for &'a G1Projective {
|
||||
type Output = G1Projective;
|
||||
|
||||
#[inline]
|
||||
fn sub(self, rhs: &'b G1Projective) -> G1Projective {
|
||||
self + (-rhs)
|
||||
}
|
||||
}
|
||||
|
||||
impl_binops_additive!(G1Projective, G1Projective);
|
||||
|
||||
impl G1Projective {
|
||||
/// Returns the identity of the group: the point at infinity.
|
||||
pub fn identity() -> G1Projective {
|
||||
|
@ -258,6 +303,75 @@ impl G1Projective {
|
|||
G1Projective::conditional_select(&tmp, &G1Projective::identity(), self.is_identity())
|
||||
}
|
||||
|
||||
/// Adds this point to another point.
|
||||
pub fn add(&self, rhs: &G1Projective) -> G1Projective {
|
||||
// This Jacobian point addition technique is based on the implementation in libsecp256k1,
|
||||
// which assumes that rhs has z=1. Let's address the case of zero z-coordinates generally.
|
||||
|
||||
// If self is the identity, return rhs. Otherwise, return self. The other cases will be
|
||||
// predicated on neither self nor rhs being the identity.
|
||||
let f1 = self.is_identity();
|
||||
let res = G1Projective::conditional_select(self, rhs, f1);
|
||||
let f2 = rhs.is_identity();
|
||||
|
||||
// If neither are the identity but x1 = x2 and y1 != y2, then return the identity
|
||||
let z = rhs.z.square();
|
||||
let u1 = self.x * z;
|
||||
let z = z * rhs.z;
|
||||
let s1 = self.y * z;
|
||||
let z = self.z.square();
|
||||
let u2 = rhs.x * z;
|
||||
let z = z * self.z;
|
||||
let s2 = rhs.y * z;
|
||||
let f3 = u1.ct_eq(&u2) & (!s1.ct_eq(&s2));
|
||||
let res =
|
||||
G1Projective::conditional_select(&res, &G1Projective::identity(), (!f1) & (!f2) & f3);
|
||||
|
||||
let t = u1 + u2;
|
||||
let m = s1 + s2;
|
||||
let rr = t.square();
|
||||
let m_alt = -u2;
|
||||
let tt = u1 * m_alt;
|
||||
let rr = rr + tt;
|
||||
|
||||
// Correct for x1 != x2 but y1 = -y2, which can occur because p - 1 is divisible by 3.
|
||||
// libsecp256k1 does this by substituting in an alternative (defined) expression for lambda.
|
||||
let degenerate = m.is_zero() & rr.is_zero();
|
||||
let rr_alt = s1 + s1;
|
||||
let m_alt = m_alt + u1;
|
||||
let rr_alt = Fp::conditional_select(&rr_alt, &rr, !degenerate);
|
||||
let m_alt = Fp::conditional_select(&m_alt, &m, !degenerate);
|
||||
|
||||
let n = m_alt.square();
|
||||
let q = n * t;
|
||||
|
||||
let n = n.square();
|
||||
let n = Fp::conditional_select(&n, &m, degenerate);
|
||||
let t = rr_alt.square();
|
||||
let z3 = m_alt * self.z * rhs.z; // We allow rhs.z != 1, so we must account for this.
|
||||
let z3 = z3 + z3;
|
||||
let q = -q;
|
||||
let t = t + q;
|
||||
let x3 = t;
|
||||
let t = t + t;
|
||||
let t = t + q;
|
||||
let t = t * rr_alt;
|
||||
let t = t + n;
|
||||
let y3 = -t;
|
||||
let x3 = x3 + x3;
|
||||
let x3 = x3 + x3;
|
||||
let y3 = y3 + y3;
|
||||
let y3 = y3 + y3;
|
||||
|
||||
let tmp = G1Projective {
|
||||
x: x3,
|
||||
y: y3,
|
||||
z: z3,
|
||||
};
|
||||
|
||||
G1Projective::conditional_select(&res, &tmp, (!f1) & (!f2) & (!f3))
|
||||
}
|
||||
|
||||
/// Returns true if this element is the identity (the point at infinity).
|
||||
#[inline]
|
||||
pub fn is_identity(&self) -> Choice {
|
||||
|
@ -461,3 +575,131 @@ fn test_doubling() {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_projective_addition() {
|
||||
{
|
||||
let a = G1Projective::identity();
|
||||
let b = G1Projective::identity();
|
||||
let c = a + b;
|
||||
assert!(bool::from(c.is_identity()));
|
||||
assert!(bool::from(c.is_on_curve()));
|
||||
}
|
||||
{
|
||||
let a = G1Projective::identity();
|
||||
let mut b = G1Projective::generator();
|
||||
{
|
||||
let z = Fp::from_raw_unchecked([
|
||||
0xba7afa1f9a6fe250,
|
||||
0xfa0f5b595eafe731,
|
||||
0x3bdc477694c306e7,
|
||||
0x2149be4b3949fa24,
|
||||
0x64aa6e0649b2078c,
|
||||
0x12b108ac33643c3e,
|
||||
]);
|
||||
|
||||
b = G1Projective {
|
||||
x: b.x * (z.square()),
|
||||
y: b.y * (z.square() * z),
|
||||
z,
|
||||
};
|
||||
}
|
||||
let c = a + b;
|
||||
assert!(!bool::from(c.is_identity()));
|
||||
assert!(bool::from(c.is_on_curve()));
|
||||
assert!(c == G1Projective::generator());
|
||||
}
|
||||
{
|
||||
let a = G1Projective::identity();
|
||||
let mut b = G1Projective::generator();
|
||||
{
|
||||
let z = Fp::from_raw_unchecked([
|
||||
0xba7afa1f9a6fe250,
|
||||
0xfa0f5b595eafe731,
|
||||
0x3bdc477694c306e7,
|
||||
0x2149be4b3949fa24,
|
||||
0x64aa6e0649b2078c,
|
||||
0x12b108ac33643c3e,
|
||||
]);
|
||||
|
||||
b = G1Projective {
|
||||
x: b.x * (z.square()),
|
||||
y: b.y * (z.square() * z),
|
||||
z,
|
||||
};
|
||||
}
|
||||
let c = b + a;
|
||||
assert!(!bool::from(c.is_identity()));
|
||||
assert!(bool::from(c.is_on_curve()));
|
||||
assert!(c == G1Projective::generator());
|
||||
}
|
||||
{
|
||||
let a = G1Projective::generator().double().double(); // 4P
|
||||
let b = G1Projective::generator().double(); // 2P
|
||||
let c = a + b;
|
||||
|
||||
let mut d = G1Projective::generator();
|
||||
for _ in 0..5 {
|
||||
d = d + G1Projective::generator();
|
||||
}
|
||||
assert!(!bool::from(c.is_identity()));
|
||||
assert!(bool::from(c.is_on_curve()));
|
||||
assert!(!bool::from(d.is_identity()));
|
||||
assert!(bool::from(d.is_on_curve()));
|
||||
assert_eq!(c, d);
|
||||
}
|
||||
|
||||
// Degenerate case
|
||||
{
|
||||
let beta = Fp::from_raw_unchecked([
|
||||
0xcd03c9e48671f071,
|
||||
0x5dab22461fcda5d2,
|
||||
0x587042afd3851b95,
|
||||
0x8eb60ebe01bacb9e,
|
||||
0x3f97d6e83d050d2,
|
||||
0x18f0206554638741,
|
||||
]);
|
||||
let beta = beta.square();
|
||||
let a = G1Projective::generator().double().double();
|
||||
let b = G1Projective {
|
||||
x: a.x * beta,
|
||||
y: -a.y,
|
||||
z: a.z,
|
||||
};
|
||||
assert!(bool::from(a.is_on_curve()));
|
||||
assert!(bool::from(b.is_on_curve()));
|
||||
|
||||
let c = a + b;
|
||||
assert_eq!(
|
||||
G1Affine::from(c),
|
||||
G1Affine::from(G1Projective {
|
||||
x: Fp::from_raw_unchecked([
|
||||
0x29e1e987ef68f2d0,
|
||||
0xc5f3ec531db03233,
|
||||
0xacd6c4b6ca19730f,
|
||||
0x18ad9e827bc2bab7,
|
||||
0x46e3b2c5785cc7a9,
|
||||
0x7e571d42d22ddd6
|
||||
]),
|
||||
y: Fp::from_raw_unchecked([
|
||||
0x94d117a7e5a539e7,
|
||||
0x8e17ef673d4b5d22,
|
||||
0x9d746aaf508a33ea,
|
||||
0x8c6d883d2516c9a2,
|
||||
0xbc3b8d5fb0447f7,
|
||||
0x7bfa4c7210f4f44
|
||||
]),
|
||||
z: Fp::one()
|
||||
})
|
||||
);
|
||||
assert!(!bool::from(c.is_identity()));
|
||||
assert!(bool::from(c.is_on_curve()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_projective_negation_and_subtraction() {
|
||||
let a = G1Projective::generator().double();
|
||||
assert_eq!(a + (-a), G1Projective::identity());
|
||||
assert_eq!(a + (-a), a - a);
|
||||
}
|
||||
|
|
287
src/g2.rs
287
src/g2.rs
|
@ -1,8 +1,11 @@
|
|||
//! This module provides an implementation of the $\mathbb{G}_2$ group of BLS12-381.
|
||||
|
||||
use core::ops::{Add, AddAssign, Neg, Sub, SubAssign};
|
||||
|
||||
use subtle::{Choice, ConditionallySelectable, ConstantTimeEq};
|
||||
|
||||
use crate::fp::Fp;
|
||||
use crate::fp2::Fp2;
|
||||
use subtle::{Choice, ConditionallySelectable, ConstantTimeEq};
|
||||
|
||||
/// This is an element of $\mathbb{G}_2$ represented in the affine coordinate space.
|
||||
/// It is ideal to keep elements in this representation to reduce memory usage and
|
||||
|
@ -223,6 +226,48 @@ impl PartialEq for G2Projective {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a> Neg for &'a G2Projective {
|
||||
type Output = G2Projective;
|
||||
|
||||
#[inline]
|
||||
fn neg(self) -> G2Projective {
|
||||
G2Projective {
|
||||
x: self.x,
|
||||
y: -self.y,
|
||||
z: self.z,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Neg for G2Projective {
|
||||
type Output = G2Projective;
|
||||
|
||||
#[inline]
|
||||
fn neg(self) -> G2Projective {
|
||||
-&self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> Add<&'b G2Projective> for &'a G2Projective {
|
||||
type Output = G2Projective;
|
||||
|
||||
#[inline]
|
||||
fn add(self, rhs: &'b G2Projective) -> G2Projective {
|
||||
self.add(rhs)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> Sub<&'b G2Projective> for &'a G2Projective {
|
||||
type Output = G2Projective;
|
||||
|
||||
#[inline]
|
||||
fn sub(self, rhs: &'b G2Projective) -> G2Projective {
|
||||
self + (-rhs)
|
||||
}
|
||||
}
|
||||
|
||||
impl_binops_additive!(G2Projective, G2Projective);
|
||||
|
||||
impl G2Projective {
|
||||
/// Returns the identity of the group: the point at infinity.
|
||||
pub fn identity() -> G2Projective {
|
||||
|
@ -309,6 +354,75 @@ impl G2Projective {
|
|||
G2Projective::conditional_select(&tmp, &G2Projective::identity(), self.is_identity())
|
||||
}
|
||||
|
||||
/// Adds this point to another point.
|
||||
pub fn add(&self, rhs: &G2Projective) -> G2Projective {
|
||||
// This Jacobian point addition technique is based on the implementation in libsecp256k1,
|
||||
// which assumes that rhs has z=1. Let's address the case of zero z-coordinates generally.
|
||||
|
||||
// If self is the identity, return rhs. Otherwise, return self. The other cases will be
|
||||
// predicated on neither self nor rhs being the identity.
|
||||
let f1 = self.is_identity();
|
||||
let res = G2Projective::conditional_select(self, rhs, f1);
|
||||
let f2 = rhs.is_identity();
|
||||
|
||||
// If neither are the identity but x1 = x2 and y1 != y2, then return the identity
|
||||
let z = rhs.z.square();
|
||||
let u1 = self.x * z;
|
||||
let z = z * rhs.z;
|
||||
let s1 = self.y * z;
|
||||
let z = self.z.square();
|
||||
let u2 = rhs.x * z;
|
||||
let z = z * self.z;
|
||||
let s2 = rhs.y * z;
|
||||
let f3 = u1.ct_eq(&u2) & (!s1.ct_eq(&s2));
|
||||
let res =
|
||||
G2Projective::conditional_select(&res, &G2Projective::identity(), (!f1) & (!f2) & f3);
|
||||
|
||||
let t = u1 + u2;
|
||||
let m = s1 + s2;
|
||||
let rr = t.square();
|
||||
let m_alt = -u2;
|
||||
let tt = u1 * m_alt;
|
||||
let rr = rr + tt;
|
||||
|
||||
// Correct for x1 != x2 but y1 = -y2, which can occur because p - 1 is divisible by 3.
|
||||
// libsecp256k1 does this by substituting in an alternative (defined) expression for lambda.
|
||||
let degenerate = m.is_zero() & rr.is_zero();
|
||||
let rr_alt = s1 + s1;
|
||||
let m_alt = m_alt + u1;
|
||||
let rr_alt = Fp2::conditional_select(&rr_alt, &rr, !degenerate);
|
||||
let m_alt = Fp2::conditional_select(&m_alt, &m, !degenerate);
|
||||
|
||||
let n = m_alt.square();
|
||||
let q = n * t;
|
||||
|
||||
let n = n.square();
|
||||
let n = Fp2::conditional_select(&n, &m, degenerate);
|
||||
let t = rr_alt.square();
|
||||
let z3 = m_alt * self.z * rhs.z; // We allow rhs.z != 1, so we must account for this.
|
||||
let z3 = z3 + z3;
|
||||
let q = -q;
|
||||
let t = t + q;
|
||||
let x3 = t;
|
||||
let t = t + t;
|
||||
let t = t + q;
|
||||
let t = t * rr_alt;
|
||||
let t = t + n;
|
||||
let y3 = -t;
|
||||
let x3 = x3 + x3;
|
||||
let x3 = x3 + x3;
|
||||
let y3 = y3 + y3;
|
||||
let y3 = y3 + y3;
|
||||
|
||||
let tmp = G2Projective {
|
||||
x: x3,
|
||||
y: y3,
|
||||
z: z3,
|
||||
};
|
||||
|
||||
G2Projective::conditional_select(&res, &tmp, (!f1) & (!f2) & (!f3))
|
||||
}
|
||||
|
||||
/// Returns true if this element is the identity (the point at infinity).
|
||||
#[inline]
|
||||
pub fn is_identity(&self) -> Choice {
|
||||
|
@ -562,3 +676,174 @@ fn test_doubling() {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_projective_addition() {
|
||||
{
|
||||
let a = G2Projective::identity();
|
||||
let b = G2Projective::identity();
|
||||
let c = a + b;
|
||||
assert!(bool::from(c.is_identity()));
|
||||
assert!(bool::from(c.is_on_curve()));
|
||||
}
|
||||
{
|
||||
let a = G2Projective::identity();
|
||||
let mut b = G2Projective::generator();
|
||||
{
|
||||
let z = Fp2 {
|
||||
c0: Fp::from_raw_unchecked([
|
||||
0xba7afa1f9a6fe250,
|
||||
0xfa0f5b595eafe731,
|
||||
0x3bdc477694c306e7,
|
||||
0x2149be4b3949fa24,
|
||||
0x64aa6e0649b2078c,
|
||||
0x12b108ac33643c3e,
|
||||
]),
|
||||
c1: Fp::from_raw_unchecked([
|
||||
0x125325df3d35b5a8,
|
||||
0xdc469ef5555d7fe3,
|
||||
0x2d716d2443106a9,
|
||||
0x5a1db59a6ff37d0,
|
||||
0x7cf7784e5300bb8f,
|
||||
0x16a88922c7a5e844,
|
||||
]),
|
||||
};
|
||||
|
||||
b = G2Projective {
|
||||
x: b.x * (z.square()),
|
||||
y: b.y * (z.square() * z),
|
||||
z,
|
||||
};
|
||||
}
|
||||
let c = a + b;
|
||||
assert!(!bool::from(c.is_identity()));
|
||||
assert!(bool::from(c.is_on_curve()));
|
||||
assert!(c == G2Projective::generator());
|
||||
}
|
||||
{
|
||||
let a = G2Projective::identity();
|
||||
let mut b = G2Projective::generator();
|
||||
{
|
||||
let z = Fp2 {
|
||||
c0: Fp::from_raw_unchecked([
|
||||
0xba7afa1f9a6fe250,
|
||||
0xfa0f5b595eafe731,
|
||||
0x3bdc477694c306e7,
|
||||
0x2149be4b3949fa24,
|
||||
0x64aa6e0649b2078c,
|
||||
0x12b108ac33643c3e,
|
||||
]),
|
||||
c1: Fp::from_raw_unchecked([
|
||||
0x125325df3d35b5a8,
|
||||
0xdc469ef5555d7fe3,
|
||||
0x2d716d2443106a9,
|
||||
0x5a1db59a6ff37d0,
|
||||
0x7cf7784e5300bb8f,
|
||||
0x16a88922c7a5e844,
|
||||
]),
|
||||
};
|
||||
|
||||
b = G2Projective {
|
||||
x: b.x * (z.square()),
|
||||
y: b.y * (z.square() * z),
|
||||
z,
|
||||
};
|
||||
}
|
||||
let c = b + a;
|
||||
assert!(!bool::from(c.is_identity()));
|
||||
assert!(bool::from(c.is_on_curve()));
|
||||
assert!(c == G2Projective::generator());
|
||||
}
|
||||
{
|
||||
let a = G2Projective::generator().double().double(); // 4P
|
||||
let b = G2Projective::generator().double(); // 2P
|
||||
let c = a + b;
|
||||
|
||||
let mut d = G2Projective::generator();
|
||||
for _ in 0..5 {
|
||||
d = d + G2Projective::generator();
|
||||
}
|
||||
assert!(!bool::from(c.is_identity()));
|
||||
assert!(bool::from(c.is_on_curve()));
|
||||
assert!(!bool::from(d.is_identity()));
|
||||
assert!(bool::from(d.is_on_curve()));
|
||||
assert_eq!(c, d);
|
||||
}
|
||||
|
||||
// Degenerate case
|
||||
{
|
||||
let beta = Fp2 {
|
||||
c0: Fp::from_raw_unchecked([
|
||||
0xcd03c9e48671f071,
|
||||
0x5dab22461fcda5d2,
|
||||
0x587042afd3851b95,
|
||||
0x8eb60ebe01bacb9e,
|
||||
0x3f97d6e83d050d2,
|
||||
0x18f0206554638741,
|
||||
]),
|
||||
c1: Fp::zero(),
|
||||
};
|
||||
let beta = beta.square();
|
||||
let a = G2Projective::generator().double().double();
|
||||
let b = G2Projective {
|
||||
x: a.x * beta,
|
||||
y: -a.y,
|
||||
z: a.z,
|
||||
};
|
||||
assert!(bool::from(a.is_on_curve()));
|
||||
assert!(bool::from(b.is_on_curve()));
|
||||
|
||||
let c = a + b;
|
||||
assert_eq!(
|
||||
G2Affine::from(c),
|
||||
G2Affine::from(G2Projective {
|
||||
x: Fp2 {
|
||||
c0: Fp::from_raw_unchecked([
|
||||
0x705abc799ca773d3,
|
||||
0xfe132292c1d4bf08,
|
||||
0xf37ece3e07b2b466,
|
||||
0x887e1c43f447e301,
|
||||
0x1e0970d033bc77e8,
|
||||
0x1985c81e20a693f2
|
||||
]),
|
||||
c1: Fp::from_raw_unchecked([
|
||||
0x1d79b25db36ab924,
|
||||
0x23948e4d529639d3,
|
||||
0x471ba7fb0d006297,
|
||||
0x2c36d4b4465dc4c0,
|
||||
0x82bbc3cfec67f538,
|
||||
0x51d2728b67bf952
|
||||
])
|
||||
},
|
||||
y: Fp2 {
|
||||
c0: Fp::from_raw_unchecked([
|
||||
0x41b1bbf6576c0abf,
|
||||
0xb6cc93713f7a0f9a,
|
||||
0x6b65b43e48f3f01f,
|
||||
0xfb7a4cfcaf81be4f,
|
||||
0x3e32dadc6ec22cb6,
|
||||
0xbb0fc49d79807e3
|
||||
]),
|
||||
c1: Fp::from_raw_unchecked([
|
||||
0x7d1397788f5f2ddf,
|
||||
0xab2907144ff0d8e8,
|
||||
0x5b7573e0cdb91f92,
|
||||
0x4cb8932dd31daf28,
|
||||
0x62bbfac6db052a54,
|
||||
0x11f95c16d14c3bbe
|
||||
])
|
||||
},
|
||||
z: Fp2::one()
|
||||
})
|
||||
);
|
||||
assert!(!bool::from(c.is_identity()));
|
||||
assert!(bool::from(c.is_on_curve()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_projective_negation_and_subtraction() {
|
||||
let a = G2Projective::generator().double();
|
||||
assert_eq!(a + (-a), G2Projective::identity());
|
||||
assert_eq!(a + (-a), a - a);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue