commit
6ee0a795f3
|
@ -0,0 +1,464 @@
|
|||
/*
|
||||
Implementation of the ZK Range Proof scheme, based on:
|
||||
Efficient Protocols for Set Membership and Range Proofs
|
||||
Jan Camenisch, Rafik Chaabouni, and abhi shelat
|
||||
Asiacrypt 2008
|
||||
*/
|
||||
extern crate pairing;
|
||||
extern crate rand;
|
||||
|
||||
use rand::{thread_rng, Rng};
|
||||
use super::*;
|
||||
use cl::{KeyPair, Signature, PublicParams, setup};
|
||||
use ped92::{CSParams, Commitment};
|
||||
use pairing::{Engine, CurveProjective};
|
||||
use ff::PrimeField;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Display;
|
||||
use std::mem::transmute;
|
||||
|
||||
/*
|
||||
paramsUL contains elements generated by the verifier, which are necessary for the prover.
|
||||
This must be computed in a trusted setup.
|
||||
*/
|
||||
#[derive(Clone)]
|
||||
struct ParamsUL<E: Engine> {
|
||||
pub mpk: PublicParams<E>,
|
||||
pub signatures: HashMap<String, Signature<E>>,
|
||||
pub com: CSParams<E>,
|
||||
kp: KeyPair<E>,
|
||||
// u determines the amount of signatures we need in the public params.
|
||||
// Each signature can be compressed to just 1 field element of 256 bits.
|
||||
// Then the parameters have minimum size equal to 256*u bits.
|
||||
u: i64,
|
||||
// l determines how many pairings we need to compute, then in order to improve
|
||||
// verifier`s performance we want to minize it.
|
||||
// Namely, we have 2*l pairings for the prover and 3*l for the verifier.
|
||||
l: i64,
|
||||
}
|
||||
|
||||
/*
|
||||
proofUL contains the necessary elements for the ZK range proof.
|
||||
*/
|
||||
#[derive(Clone)]
|
||||
struct ProofUL<E: Engine> {
|
||||
V: Vec<(E::G1, E::G1)>,
|
||||
D: E::G2,
|
||||
comm: Commitment<E>,
|
||||
a: Vec<E::Fqk>,
|
||||
zx: Vec<E::Fr>,
|
||||
zsig: Vec<E::Fr>,
|
||||
zv: Vec<E::Fr>,
|
||||
ch: E::Fr,
|
||||
zr: E::Fr,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RangeProof<E: Engine> {
|
||||
p1: ProofUL<E>,
|
||||
p2: ProofUL<E>,
|
||||
}
|
||||
/*
|
||||
params contains elements generated by the verifier, which are necessary for the prover.
|
||||
This must be computed in a trusted setup.
|
||||
*/
|
||||
#[derive(Clone)]
|
||||
pub struct RPPublicParams<E: Engine> {
|
||||
p: ParamsUL<E>,
|
||||
a: i64,
|
||||
b: i64,
|
||||
}
|
||||
|
||||
impl<E: Engine> ParamsUL<E> {
|
||||
/*
|
||||
setup_ul generates the signature for the interval [0,u^l).
|
||||
The value of u should be roughly b/log(b), but we can choose smaller values in
|
||||
order to get smaller parameters, at the cost of having worse performance.
|
||||
*/
|
||||
pub fn setup_ul<R: Rng>(rng: &mut R, u: i64, l: i64) -> Self {
|
||||
let mpk = setup(rng);
|
||||
let kp = KeyPair::<E>::generate(rng, &mpk, 1);
|
||||
|
||||
let mut signatures: HashMap<String, Signature<E>> = HashMap::new();
|
||||
for i in 0..u {
|
||||
let sig_i = kp.sign(rng, &vec! {E::Fr::from_str(i.to_string().as_str()).unwrap()});
|
||||
signatures.insert(i.to_string(), sig_i);
|
||||
}
|
||||
|
||||
let com = CSParams::setup(rng);
|
||||
return ParamsUL { mpk, signatures, com, kp, u, l };
|
||||
}
|
||||
|
||||
/*
|
||||
prove_ul method is used to produce the ZKRP proof that secret x belongs to the interval [0,U^L].
|
||||
*/
|
||||
pub fn prove_ul<R: Rng>(&self, rng: &mut R, x: i64, r: E::Fr) -> ProofUL<E> {
|
||||
//TODO: check if x in range
|
||||
let decx = decompose(x, self.u, self.l);
|
||||
let modx = E::Fr::from_str(&(x.to_string())).unwrap();
|
||||
|
||||
// Initialize variables
|
||||
let mut v = Vec::<E::Fr>::with_capacity(self.l as usize);
|
||||
let mut V = Vec::<(E::G1, E::G1)>::with_capacity(self.l as usize);
|
||||
let mut a = Vec::<E::Fqk>::with_capacity(self.l as usize);
|
||||
let mut s = Vec::<E::Fr>::with_capacity(self.l as usize);
|
||||
let mut t = Vec::<E::Fr>::with_capacity(self.l as usize);
|
||||
let mut tt = Vec::<E::Fr>::with_capacity(self.l as usize);
|
||||
let mut zx = Vec::<E::Fr>::with_capacity(self.l as usize);
|
||||
let mut zsig = Vec::<E::Fr>::with_capacity(self.l as usize);
|
||||
let mut zv = Vec::<E::Fr>::with_capacity(self.l as usize);
|
||||
let mut D = E::G2::zero();
|
||||
let m = E::Fr::rand(rng);
|
||||
|
||||
// D = H^m
|
||||
let mut hm = self.com.h.clone();
|
||||
hm.mul_assign(m);
|
||||
for i in 0..self.l as usize {
|
||||
v.push(E::Fr::rand(rng));
|
||||
let r2 = E::Fr::rand(rng);
|
||||
let signature = self.signatures.get(&decx[i].to_string()).unwrap();
|
||||
let mut A = signature.h;
|
||||
let mut B = signature.H;
|
||||
let mut Aprime = A.clone();
|
||||
A.mul_assign(r2);
|
||||
Aprime.mul_assign(v[i]);
|
||||
B.add_assign(&Aprime);
|
||||
B.mul_assign(r2);
|
||||
V.push((A, B));
|
||||
s.push(E::Fr::rand(rng));
|
||||
let mut gx = E::pairing(V[i].0, self.kp.public.X);
|
||||
gx = gx.pow(s[i].into_repr());
|
||||
a.push(gx);
|
||||
t.push(E::Fr::rand(rng));
|
||||
assert_eq!(self.kp.public.Y.len(), 1);
|
||||
let mut gy = E::pairing(V[i].0, self.kp.public.Y[0]);
|
||||
gy = gy.pow(t[i].into_repr());
|
||||
a[i].mul_assign(&gy);
|
||||
tt.push(E::Fr::rand(rng));
|
||||
let mut h = E::pairing(V[i].0, self.mpk.g2);
|
||||
h = h.pow(tt[i].into_repr());
|
||||
a[i].mul_assign(&h);
|
||||
|
||||
let ui = self.u.pow(i as u32);
|
||||
let mut muiti = t[i].clone();
|
||||
muiti.mul_assign(&E::Fr::from_str(&ui.to_string()).unwrap());
|
||||
let mut aux = self.com.g.clone();
|
||||
aux.mul_assign(muiti);
|
||||
D.add_assign(&aux);
|
||||
}
|
||||
D.add_assign(&hm);
|
||||
|
||||
let C = self.com.commit(rng, modx, Some(r));
|
||||
// Fiat-Shamir heuristic
|
||||
let c = Hash::<E>(a.clone(), D.clone());
|
||||
|
||||
let mut zr = m.clone();
|
||||
let mut rc = r.clone();
|
||||
rc.mul_assign(&c);
|
||||
zr.add_assign(&rc);
|
||||
for i in 0..self.l as usize {
|
||||
zsig.push(t[i].clone());
|
||||
let mut dx = E::Fr::from_str(&decx[i].to_string()).unwrap();
|
||||
dx.mul_assign(&c);
|
||||
zsig[i].add_assign(&dx);
|
||||
zx.push(s[i].clone());
|
||||
zx[i].add_assign(&c);
|
||||
zv.push(tt[i].clone());
|
||||
let mut vic = v[i].clone();
|
||||
vic.mul_assign(&c);
|
||||
zv[i].add_assign(&vic);
|
||||
}
|
||||
|
||||
return ProofUL { V, D, comm: C, a, zx, zsig, zv, ch: c, zr };
|
||||
}
|
||||
|
||||
/*
|
||||
verify_ul is used to validate the ZKRP proof. It returns true iff the proof is valid.
|
||||
*/
|
||||
pub fn verify_ul(&self, proof: &ProofUL<E>) -> bool {
|
||||
// D == C^c.h^ zr.g^zsig ?
|
||||
let r1 = self.verify_part1(&proof);
|
||||
let r2 = self.verify_part2(&proof);
|
||||
return r1 && r2;
|
||||
}
|
||||
|
||||
fn verify_part2(&self, proof: &ProofUL<E>) -> bool {
|
||||
let mut r2 = true;
|
||||
for i in 0..self.l as usize {
|
||||
let mut gx = E::pairing(proof.V[i].0, self.kp.public.X);
|
||||
gx = gx.pow(proof.zx[i].into_repr());
|
||||
|
||||
let mut gy = E::pairing(proof.V[i].0, self.kp.public.Y[0]);
|
||||
gy = gy.pow(proof.zsig[i].into_repr());
|
||||
gx.mul_assign(&gy);
|
||||
|
||||
let mut h = E::pairing(proof.V[i].0, self.mpk.g2);
|
||||
h = h.pow(proof.zv[i].into_repr());
|
||||
gx.mul_assign(&h);
|
||||
|
||||
let mut g = E::pairing(proof.V[i].1, self.mpk.g2);
|
||||
g = g.pow(proof.ch.into_repr());
|
||||
g.mul_assign(&proof.a[i]);
|
||||
r2 = r2 && gx == g;
|
||||
}
|
||||
return r2;
|
||||
}
|
||||
|
||||
fn verify_part1(&self, proof: &ProofUL<E>) -> bool {
|
||||
let mut D = proof.comm.c.clone();
|
||||
D.mul_assign(proof.ch);
|
||||
D.negate();
|
||||
let mut hzr = self.com.h.clone();
|
||||
hzr.mul_assign(proof.zr);
|
||||
D.add_assign(&hzr);
|
||||
for i in 0..self.l {
|
||||
let ui = self.u.pow(i as u32);
|
||||
let mut muizsigi = proof.zsig[i as usize];
|
||||
muizsigi.mul_assign(&E::Fr::from_str(&ui.to_string()).unwrap());
|
||||
let mut aux = self.com.g.clone();
|
||||
aux.mul_assign(muizsigi);
|
||||
D.add_assign(&aux);
|
||||
}
|
||||
return D == proof.D;
|
||||
}
|
||||
}
|
||||
|
||||
fn Hash<E: Engine>(a: Vec<E::Fqk>, D: E::G2) -> E::Fr {
|
||||
// create a Sha256 object
|
||||
let mut a_vec: Vec<u8> = Vec::new();
|
||||
for a_el in a {
|
||||
a_vec.extend(format!("{}", a_el).bytes());
|
||||
}
|
||||
|
||||
let mut x_vec: Vec<u8> = Vec::new();
|
||||
x_vec.extend(format!("{}", D).bytes());
|
||||
a_vec.extend(x_vec);
|
||||
let sha2_digest = sha512::hash(a_vec.as_slice());
|
||||
|
||||
let mut hash_buf: [u8; 64] = [0; 64];
|
||||
hash_buf.copy_from_slice(&sha2_digest[0..64]);
|
||||
let hexresult = fmt_bytes_to_int(hash_buf);
|
||||
let result = E::Fr::from_str(&hexresult);
|
||||
return result.unwrap();
|
||||
}
|
||||
|
||||
/*
|
||||
Decompose receives as input an integer x and outputs an array of integers such that
|
||||
x = sum(xi.u^i), i.e. it returns the decomposition of x into base u.
|
||||
*/
|
||||
fn decompose(x: i64, u: i64, l: i64) -> Vec<i64> {
|
||||
let mut result = Vec::with_capacity(l as usize);
|
||||
let mut decomposer = x.clone();
|
||||
for _i in 0..l {
|
||||
result.push(decomposer % u);
|
||||
decomposer = decomposer / u;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
fn fmt_bytes_to_int(bytearray: [u8; 64]) -> String {
|
||||
let mut result: String = "".to_string();
|
||||
for byte in bytearray.iter() {
|
||||
// Decide if you want upper- or lowercase results,
|
||||
// padding the values to two characters, spaces
|
||||
// between bytes, etc.
|
||||
result = result + &format!("{}", *byte as u8);
|
||||
}
|
||||
result.to_string()
|
||||
}
|
||||
|
||||
impl<E: Engine> RPPublicParams<E> {
|
||||
/*
|
||||
Setup receives integers a and b, and configures the parameters for the rangeproof scheme.
|
||||
*/
|
||||
pub fn setup<R: Rng>(rng: &mut R, a: i64, b: i64) -> Self {
|
||||
// Compute optimal values for u and l
|
||||
if a > b {
|
||||
panic!("a must be less than or equal to b");
|
||||
}
|
||||
let logb = (b as f64).log10();
|
||||
if logb != 0.0 {
|
||||
let u = b / logb as i64;
|
||||
if u != 0 {
|
||||
let l = (b as f64).log(u as f64).ceil() as i64;
|
||||
let params_out: ParamsUL<E> = ParamsUL::<E>::setup_ul(rng, u, l);
|
||||
return RPPublicParams { p: params_out, a, b };
|
||||
} else {
|
||||
panic!("u is zero");
|
||||
}
|
||||
} else {
|
||||
panic!("log(b) is zero");
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Prove method is responsible for generating the zero knowledge proof.
|
||||
*/
|
||||
pub fn prove<R: Rng>(&self, rng: &mut R, x: i64) -> RangeProof<E> {
|
||||
let ul = self.p.u.pow(self.p.l as u32);
|
||||
let r = E::Fr::rand(rng);
|
||||
|
||||
// x - b + ul
|
||||
let xb = x - self.b + ul;
|
||||
let first = self.p.prove_ul(rng, xb, r);
|
||||
|
||||
// x - a
|
||||
let xa = x - self.a;
|
||||
let second = self.p.prove_ul(rng, xa, r);
|
||||
|
||||
return RangeProof { p1: first, p2: second };
|
||||
}
|
||||
|
||||
/*
|
||||
Verify is responsible for validating the proof.
|
||||
*/
|
||||
pub fn verify(&self, proof: RangeProof<E>) -> bool {
|
||||
let first = self.p.verify_ul(&proof.p1);
|
||||
let second = self.p.verify_ul(&proof.p2);
|
||||
return first && second;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pairing::bls12_381::{Bls12, G1, G2, Fq12, Fr};
|
||||
|
||||
#[test]
|
||||
fn setup_ul_works() {
|
||||
let rng = &mut rand::thread_rng();
|
||||
let params = ParamsUL::<Bls12>::setup_ul(rng, 2, 3);
|
||||
assert_eq!(params.signatures.len(), 2);
|
||||
for (m, s) in params.signatures {
|
||||
assert_eq!(params.kp.verify(¶ms.mpk, &vec! {Fr::from_str(m.to_string().as_str()).unwrap()}, &s), true);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prove_ul_works() {
|
||||
let rng = &mut rand::thread_rng();
|
||||
let params = ParamsUL::<Bls12>::setup_ul(rng, 2, 3);
|
||||
let fr = Fr::rand(rng);
|
||||
let proof = params.prove_ul(rng, 10, fr);
|
||||
assert_eq!(proof.a.len(), 3);
|
||||
assert_eq!(proof.V.len(), 3);
|
||||
assert_eq!(proof.zsig.len(), 3);
|
||||
assert_eq!(proof.zv.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prove_and_verify_part1_ul_works() {
|
||||
let rng = &mut rand::thread_rng();
|
||||
let params = ParamsUL::<Bls12>::setup_ul(rng, 2, 4);
|
||||
let fr = Fr::rand(rng);
|
||||
let proof = params.prove_ul(rng, 10, fr);
|
||||
assert_eq!(params.verify_part1(&proof), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prove_and_verify_part2_ul_works() {
|
||||
let rng = &mut rand::thread_rng();
|
||||
let params = ParamsUL::<Bls12>::setup_ul(rng, 2, 4);
|
||||
let fr = Fr::rand(rng);
|
||||
let proof = params.prove_ul(rng, 10, fr);
|
||||
assert_eq!(params.verify_part2(&proof), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prove_and_verify_ul_works() {
|
||||
let rng = &mut rand::thread_rng();
|
||||
let params = ParamsUL::<Bls12>::setup_ul(rng, 2, 4);
|
||||
let fr = Fr::rand(rng);
|
||||
let proof = params.prove_ul(rng, 10, fr);
|
||||
assert_eq!(params.verify_ul(&proof), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prove_and_verify_works() {
|
||||
let rng = &mut rand::thread_rng();
|
||||
let params = RPPublicParams::<Bls12>::setup(rng, 2, 25);
|
||||
let proof = params.prove(rng, 10);
|
||||
assert_eq!(params.verify(proof), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decompose_works() {
|
||||
assert_eq!(decompose(25, 3, 3), vec! {1, 2, 2});
|
||||
assert_eq!(decompose(336, 7, 3), vec! {0, 6, 6});
|
||||
assert_eq!(decompose(285, 8, 3), vec! {5, 3, 4});
|
||||
assert_eq!(decompose(125, 13, 2), vec! {8, 9});
|
||||
assert_eq!(decompose(143225, 6, 7), vec! {5, 2, 0, 3, 2, 0, 3});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decompose_recompose_works() {
|
||||
let vec1 = decompose(25, 3, 5);
|
||||
let mut result = 0;
|
||||
for i in 0..5 {
|
||||
result += vec1[i] * 3i64.pow(i as u32);
|
||||
}
|
||||
assert_eq!(result, 25);
|
||||
|
||||
let vec1 = decompose(143225, 6, 7);
|
||||
let mut result = 0;
|
||||
for i in 0..7 {
|
||||
result += vec1[i] * 6i64.pow(i as u32);
|
||||
}
|
||||
assert_eq!(result, 143225);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn setup_works() {
|
||||
let rng = &mut rand::thread_rng();
|
||||
let public_params = RPPublicParams::<Bls12>::setup(rng, 2, 10);
|
||||
assert_eq!(public_params.a, 2);
|
||||
assert_eq!(public_params.b, 10);
|
||||
assert_eq!(public_params.p.signatures.len(), 10);
|
||||
assert_eq!(public_params.p.u, 10 / ((10 as f64).log10() as i64));
|
||||
assert_eq!(public_params.p.l, ((10 / (10 / ((10 as f64).log10() as i64))) as f64).ceil() as i64);
|
||||
for (m, s) in public_params.p.signatures {
|
||||
assert_eq!(public_params.p.kp.verify(&public_params.p.mpk, &vec! {Fr::from_str(m.to_string().as_str()).unwrap()}, &s), true);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "a must be less than or equal to b")]
|
||||
fn setup_wrong_a_and_b() {
|
||||
let rng = &mut rand::thread_rng();
|
||||
RPPublicParams::<Bls12>::setup(rng, 10, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "u is zero")]
|
||||
fn setup_wrong_b() {
|
||||
let rng = &mut rand::thread_rng();
|
||||
RPPublicParams::<Bls12>::setup(rng, -1, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "log(b) is zero")]
|
||||
fn setup_wrong_logb() {
|
||||
let rng = &mut rand::thread_rng();
|
||||
RPPublicParams::<Bls12>::setup(rng, -1, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fmt_byte_to_int_works() {
|
||||
assert_eq!(fmt_bytes_to_int([12, 235, 23, 123, 13, 43, 12, 235, 23, 123, 13, 43, 12, 235, 23, 123, 13, 43, 12, 235, 23, 123, 13, 43, 12, 235, 23, 123, 13, 43, 12, 235, 23, 123, 13, 43, 12, 235, 23, 123, 13, 43, 12, 235, 23, 123, 13, 43, 12, 235, 23, 123, 13, 43, 12, 235, 23, 123, 13, 43, 12, 235, 23, 123]),
|
||||
"122352312313431223523123134312235231231343122352312313431223523123134312235231231343122352312313431223523123134312235231231343122352312313431223523123");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hash_works() {
|
||||
let rng = &mut rand::thread_rng();
|
||||
let D = G2::rand(rng);
|
||||
let D2 = G2::rand(rng);
|
||||
let a = vec! {Fq12::rand(rng), Fq12::rand(rng), Fq12::rand(rng)};
|
||||
let a2 = vec! {Fq12::rand(rng), Fq12::rand(rng), Fq12::rand(rng)};
|
||||
assert_eq!(Hash::<Bls12>(a.clone(), D.clone()).is_zero(), false);
|
||||
assert_ne!(Hash::<Bls12>(a2.clone(), D.clone()), Hash::<Bls12>(a.clone(), D.clone()));
|
||||
assert_ne!(Hash::<Bls12>(a.clone(), D2.clone()), Hash::<Bls12>(a.clone(), D.clone()));
|
||||
assert_ne!(Hash::<Bls12>(a2.clone(), D2.clone()), Hash::<Bls12>(a.clone(), D.clone()));
|
||||
}
|
||||
}
|
|
@ -64,6 +64,7 @@ pub mod sym;
|
|||
pub mod ote;
|
||||
pub mod cl;
|
||||
pub mod clsigs;
|
||||
pub mod ccs08;
|
||||
pub mod commit_scheme;
|
||||
pub mod ped92;
|
||||
pub mod clproto;
|
||||
|
|
14
src/ped92.rs
14
src/ped92.rs
|
@ -1,12 +1,12 @@
|
|||
// ped92.rs
|
||||
use rand::{thread_rng, Rng};
|
||||
use pairing::{Engine, CurveProjective, CurveAffine};
|
||||
use pairing::{Engine, CurveProjective};
|
||||
use ff::Rand;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CSParams<E: Engine> {
|
||||
g: E::G2,
|
||||
h: E::G2,
|
||||
pub g: E::G2,
|
||||
pub h: E::G2,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
@ -164,8 +164,8 @@ mod tests {
|
|||
let r = Fr::rand(rng);
|
||||
let c = csp.commit(rng, m1, Some(r));
|
||||
|
||||
assert_eq!(true, csp.decommit(&c, &m1, &r));
|
||||
assert_eq!(false, csp.decommit(&c, &m2, &r));
|
||||
assert_eq!(csp.decommit(&c, &m1, &r), true);
|
||||
assert_eq!(csp.decommit(&c, &m2, &r), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -181,9 +181,9 @@ mod tests {
|
|||
let r = m[0].clone();
|
||||
let c = csp.commit(rng, &m, &r);
|
||||
|
||||
assert_eq!(true, csp.decommit(&c, &m, &r));
|
||||
assert_eq!(csp.decommit(&c, &m, &r), true);
|
||||
let mut r1 = r.clone();
|
||||
r1.add_assign(&Fr::one());
|
||||
assert_eq!(false, csp.decommit(&c, &m, &r1));
|
||||
assert_eq!(csp.decommit(&c, &m, &r1), false);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue