Add bilinear equations into the zk proof; the last eq passes the tests
This commit is contained in:
Generated
+63
-5
@@ -995,7 +995,11 @@ dependencies = [
|
||||
"ff 0.10.1",
|
||||
"group 0.10.0",
|
||||
"pairing 0.20.0",
|
||||
<<<<<<< HEAD
|
||||
"rand_core 0.6.4",
|
||||
=======
|
||||
"rand_core 0.6.3",
|
||||
>>>>>>> b6a43787b (Add bilinear equations into the zk proof; the last eq passes the tests)
|
||||
"subtle 2.4.1",
|
||||
]
|
||||
|
||||
@@ -1005,15 +1009,23 @@ version = "0.6.0"
|
||||
source = "git+https://github.com/jstuczyn/bls12_381?branch=gt-serialisation#10fb6f700bfda17c8475af3bfd31e3fec15f2278"
|
||||
dependencies = [
|
||||
"digest 0.9.0",
|
||||
<<<<<<< HEAD
|
||||
"ff 0.11.1",
|
||||
"group 0.11.0",
|
||||
"pairing 0.21.0",
|
||||
"rand_core 0.6.4",
|
||||
=======
|
||||
"ff 0.11.0",
|
||||
"group 0.11.0",
|
||||
"pairing 0.21.0",
|
||||
"rand_core 0.6.3",
|
||||
>>>>>>> b6a43787b (Add bilinear equations into the zk proof; the last eq passes the tests)
|
||||
"subtle 2.4.1",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
<<<<<<< HEAD
|
||||
name = "bnum"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
@@ -1041,6 +1053,8 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
=======
|
||||
>>>>>>> b6a43787b (Add bilinear equations into the zk proof; the last eq passes the tests)
|
||||
name = "bs58"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
@@ -1812,6 +1826,22 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
name = "credentials"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bls12_381 0.5.0",
|
||||
"coconut-interface",
|
||||
"crypto",
|
||||
"network-defaults",
|
||||
"thiserror",
|
||||
"url",
|
||||
"validator-client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
>>>>>>> b6a43787b (Add bilinear equations into the zk proof; the last eq passes the tests)
|
||||
name = "criterion"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
@@ -3606,6 +3636,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc5ac374b108929de78460075f3dc439fa66df9d8fc77e8f12caa5165fcf0c89"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
<<<<<<< HEAD
|
||||
"ff 0.11.1",
|
||||
"rand_core 0.6.4",
|
||||
"subtle 2.4.1",
|
||||
@@ -3630,6 +3661,10 @@ checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
|
||||
dependencies = [
|
||||
"ff 0.13.0",
|
||||
"rand_core 0.6.4",
|
||||
=======
|
||||
"ff 0.11.0",
|
||||
"rand_core 0.6.3",
|
||||
>>>>>>> b6a43787b (Add bilinear equations into the zk proof; the last eq passes the tests)
|
||||
"subtle 2.4.1",
|
||||
]
|
||||
|
||||
@@ -7686,8 +7721,13 @@ name = "nym_compact_ecash"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bls12_381 0.5.0",
|
||||
<<<<<<< HEAD
|
||||
"bs58 0.4.0",
|
||||
"criterion 0.3.6",
|
||||
=======
|
||||
"bs58",
|
||||
"criterion",
|
||||
>>>>>>> b6a43787b (Add bilinear equations into the zk proof; the last eq passes the tests)
|
||||
"digest 0.9.0",
|
||||
"ff 0.10.1",
|
||||
"group 0.10.0",
|
||||
@@ -7704,12 +7744,12 @@ name = "nymvisor"
|
||||
name = "nym_offline_divisible_ecash"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bls12_381",
|
||||
"bls12_381 0.6.0",
|
||||
"bs58",
|
||||
"criterion",
|
||||
"digest 0.9.0",
|
||||
"ff 0.10.1",
|
||||
"group 0.10.0",
|
||||
"ff 0.11.0",
|
||||
"group 0.11.0",
|
||||
"itertools",
|
||||
"rand 0.8.5",
|
||||
"sha2",
|
||||
@@ -7720,7 +7760,7 @@ dependencies = [
|
||||
name = "nym_online_divisible_ecash"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bls12_381",
|
||||
"bls12_381 0.5.0",
|
||||
"bs58",
|
||||
"criterion",
|
||||
"digest 0.9.0",
|
||||
@@ -7737,7 +7777,7 @@ name = "nymcoconut"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"bls12_381",
|
||||
"bls12_381 0.5.0",
|
||||
"bs58",
|
||||
"criterion",
|
||||
"digest 0.9.0",
|
||||
@@ -8145,6 +8185,18 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "pairing"
|
||||
version = "0.21.0"
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2e415e349a3006dd7d9482cdab1c980a845bed1377777d768cb693a44540b42"
|
||||
dependencies = [
|
||||
"group 0.11.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parity-scale-codec"
|
||||
version = "2.3.1"
|
||||
>>>>>>> b6a43787b (Add bilinear equations into the zk proof; the last eq passes the tests)
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2e415e349a3006dd7d9482cdab1c980a845bed1377777d768cb693a44540b42"
|
||||
dependencies = [
|
||||
@@ -12945,9 +12997,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
<<<<<<< HEAD
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9"
|
||||
=======
|
||||
version = "1.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d68d9dcec5f9b43a30d38c49f91dfedfaac384cb8f085faca366c26207dd1619"
|
||||
>>>>>>> b6a43787b (Add bilinear equations into the zk proof; the last eq passes the tests)
|
||||
dependencies = [
|
||||
"zeroize_derive",
|
||||
]
|
||||
|
||||
@@ -7,7 +7,7 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bls12_381 = { version = "0.5", default-features = false, features = ["pairings", "alloc", "experimental"] }
|
||||
bls12_381 = { git = "https://github.com/jstuczyn/bls12_381", branch = "gt-serialisation", default-features = false, features = ["alloc", "pairings", "experimental", "zeroize"] }
|
||||
itertools = "0.10"
|
||||
digest = "0.9"
|
||||
rand = "0.8"
|
||||
@@ -19,9 +19,9 @@ bs58 = "0.4.0"
|
||||
criterion = { version = "0.3", features = ["html_reports"] }
|
||||
|
||||
[dependencies.ff]
|
||||
version = "0.10"
|
||||
version = "0.11"
|
||||
default-features = false
|
||||
|
||||
[dependencies.group]
|
||||
version = "0.10"
|
||||
version = "0.11"
|
||||
default-features = false
|
||||
@@ -1,6 +1,11 @@
|
||||
use bls12_381::{G1Projective, G2Projective, Gt, Scalar};
|
||||
use std::convert::TryFrom;
|
||||
use std::ops::Neg;
|
||||
|
||||
use crate::scheme::{Phi, VarPhi};
|
||||
use bls12_381::{G1Projective, G2Projective, Gt, Scalar};
|
||||
use group::GroupEncoding;
|
||||
|
||||
use crate::proofs::{ChallengeDigest, compute_challenge, produce_response, produce_responses};
|
||||
use crate::scheme::{Phi, VarPhi, Wallet};
|
||||
use crate::scheme::keygen::{SecretKeyUser, VerificationKeyAuth};
|
||||
use crate::scheme::setup::Parameters;
|
||||
|
||||
@@ -8,28 +13,57 @@ pub struct SpendInstance {
|
||||
pub kappa: G2Projective,
|
||||
pub phi: Phi,
|
||||
pub varphi: VarPhi,
|
||||
pub rr: G1Projective,
|
||||
pub rr: Scalar,
|
||||
pub rr_prime: G1Projective,
|
||||
pub ss: G1Projective,
|
||||
pub tt: G2Projective,
|
||||
pub pg_varsigpr1_delta: Gt,
|
||||
pub varsig_prime1: G1Projective,
|
||||
pub theta_prime1: G1Projective,
|
||||
pub pg_eq1: Gt,
|
||||
pub pg_eq2: Gt,
|
||||
pub pg_eq3: Gt,
|
||||
pub pg_eq4: Gt,
|
||||
pub psi_g1: G1Projective,
|
||||
pub psi_g2: G2Projective,
|
||||
pub pg_psi0_delta: Gt,
|
||||
pub pg_varsigpr2_gen2: Gt,
|
||||
pub pg_psi0_gen2: Gt,
|
||||
pub pg_thetapr1_delta: Gt,
|
||||
pub pg_thetapr2_gen2: Gt,
|
||||
pub pg_rr_yy: Gt,
|
||||
pub pg_psi0_yy: Gt,
|
||||
pub pg_ssprime_gen2: Gt,
|
||||
pub pg_varsigpr2_ww1: Gt,
|
||||
pub pg_psi0_ww1: Gt,
|
||||
pub pg_thetapr2_ww2: Gt,
|
||||
pub pg_psi0_ww2: Gt,
|
||||
pub pg_gen1_zz: Gt,
|
||||
pub pg_rr_tt: Gt,
|
||||
pub pg_rr_psi1: Gt,
|
||||
pub pg_psi0_tt: Gt,
|
||||
pub pg_psi0_psi1: Gt,
|
||||
pub pg_gen1_gen2: Gt,
|
||||
}
|
||||
|
||||
impl SpendInstance {
|
||||
pub(crate) fn to_bytes(&self) -> Vec<u8> {
|
||||
let mut bytes = Vec::with_capacity(96 + 96 + 3 * 96 + 5 * 48 + 12 * 288);
|
||||
bytes.extend_from_slice(self.kappa.to_bytes().as_ref());
|
||||
bytes.extend_from_slice(self.phi.to_bytes().as_ref());
|
||||
bytes.extend_from_slice(self.varphi.to_bytes().as_ref());
|
||||
bytes.extend_from_slice(self.rr.to_bytes().as_ref());
|
||||
bytes.extend_from_slice(self.rr_prime.to_bytes().as_ref());
|
||||
bytes.extend_from_slice(self.ss.to_bytes().as_ref());
|
||||
bytes.extend_from_slice(self.tt.to_bytes().as_ref());
|
||||
bytes.extend_from_slice(self.varsig_prime1.to_bytes().as_ref());
|
||||
bytes.extend_from_slice(self.theta_prime1.to_bytes().as_ref());
|
||||
bytes.extend_from_slice(self.pg_eq1.to_compressed().as_ref());
|
||||
bytes.extend_from_slice(self.pg_eq2.to_compressed().as_ref());
|
||||
bytes.extend_from_slice(self.pg_eq3.to_compressed().as_ref());
|
||||
bytes.extend_from_slice(self.pg_eq4.to_compressed().as_ref());
|
||||
bytes.extend_from_slice(self.psi_g1.to_bytes().as_ref());
|
||||
bytes.extend_from_slice(self.psi_g2.to_bytes().as_ref());
|
||||
bytes.extend_from_slice(self.pg_psi0_delta.to_compressed().as_ref());
|
||||
bytes.extend_from_slice(self.pg_psi0_gen2.to_compressed().as_ref());
|
||||
bytes.extend_from_slice(self.pg_psi0_yy.to_compressed().as_ref());
|
||||
bytes.extend_from_slice(self.pg_psi0_ww1.to_compressed().as_ref());
|
||||
bytes.extend_from_slice(self.pg_psi0_ww2.to_compressed().as_ref());
|
||||
bytes.extend_from_slice(self.pg_rr_psi1.to_compressed().as_ref());
|
||||
bytes.extend_from_slice(self.pg_psi0_tt.to_compressed().as_ref());
|
||||
bytes.extend_from_slice(self.pg_psi0_psi1.to_compressed().as_ref());
|
||||
|
||||
bytes
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SpendWitness {
|
||||
@@ -51,16 +85,360 @@ pub struct SpendWitness {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SpendProof {}
|
||||
pub struct SpendProof {
|
||||
challenge: Scalar,
|
||||
response_r: Scalar,
|
||||
response_r_sk_u: Scalar,
|
||||
response_r_v: Scalar,
|
||||
response_r_r: Scalar,
|
||||
response_r_r1: Scalar,
|
||||
response_r_r2: Scalar,
|
||||
response_r_varsig1: Scalar,
|
||||
response_r_theta1: Scalar,
|
||||
response_r_varsig2: Scalar,
|
||||
response_r_theta2: Scalar,
|
||||
response_r_rr: Scalar,
|
||||
response_r_ss: Scalar,
|
||||
response_r_tt: Scalar,
|
||||
response_r_rho1: Scalar,
|
||||
response_r_rho2: Scalar,
|
||||
response_r_rho3: Scalar,
|
||||
}
|
||||
|
||||
impl SpendProof {
|
||||
pub fn construct(
|
||||
params: &Parameters,
|
||||
instance: &SpendInstance,
|
||||
witness: &SpendWitness) -> Self {
|
||||
SpendProof {}
|
||||
witness: &SpendWitness,
|
||||
verification_key: &VerificationKeyAuth,
|
||||
vv: u64) -> Self {
|
||||
let grp = params.get_grp();
|
||||
let params_u = params.get_params_u();
|
||||
let params_a = params.get_params_a();
|
||||
|
||||
// generate random values to replace each witness
|
||||
let r_attributes = grp.n_random_scalars(2);
|
||||
let r_sk_u = r_attributes[0];
|
||||
let r_v = r_attributes[1];
|
||||
let r_r = grp.random_scalar();
|
||||
let r_r1 = grp.random_scalar();
|
||||
let r_r2 = grp.random_scalar();
|
||||
let r_r_varsig1 = grp.random_scalar();
|
||||
let r_r_theta1 = grp.random_scalar();
|
||||
let r_r_varsig2 = grp.random_scalar();
|
||||
let r_r_theta2 = grp.random_scalar();
|
||||
let r_r_rr = grp.random_scalar();
|
||||
let r_r_ss = grp.random_scalar();
|
||||
let r_r_tt = grp.random_scalar();
|
||||
let r_rho1 = grp.random_scalar();
|
||||
let r_rho2 = grp.random_scalar();
|
||||
let r_rho3 = grp.random_scalar();
|
||||
|
||||
let g1 = grp.gen1();
|
||||
|
||||
// compute zkp commitment for each instance
|
||||
let zkcm_kappa = grp.gen2() * r_r
|
||||
+ verification_key.alpha
|
||||
+ r_attributes
|
||||
.iter()
|
||||
.zip(verification_key.beta_g2.iter())
|
||||
.map(|(attr, beta_i)| beta_i * attr)
|
||||
.sum::<G2Projective>();
|
||||
|
||||
let zkcm_phi0 = g1 * r_r1;
|
||||
let zkcm_phi1 = instance.varsig_prime1 * r_v + instance.psi_g1 * r_rho1 + params_u.get_ith_eta(vv as usize) * r_r1;
|
||||
let zkcm_varphi0 = g1 * r_r2;
|
||||
let zkcm_varphi1 = (g1 * instance.rr) * r_sk_u + instance.theta_prime1 * r_v + instance.psi_g1 * r_rho2 + params_u.get_ith_eta(vv as usize) * r_r2;
|
||||
let zkcm_pg_eq1 = instance.pg_psi0_delta * r_r_varsig1 + instance.pg_psi0_gen2 * r_r_varsig2.neg();
|
||||
let zkcm_pg_eq2 = instance.pg_psi0_delta * r_r_theta1 + instance.pg_psi0_gen2 * r_r_theta2.neg();
|
||||
let zkcm_pg_eq3 = instance.pg_psi0_yy * r_r_rr + instance.pg_psi0_gen2 * r_r_ss + instance.pg_psi0_ww1 * r_r_varsig2 + instance.pg_psi0_ww2 * r_r_theta2;
|
||||
let zkcm_pg_eq4 = instance.pg_rr_psi1 * r_r_tt + instance.pg_psi0_tt * r_r_rr + instance.pg_psi0_psi1 * r_rho3.neg();
|
||||
|
||||
|
||||
let challenge = compute_challenge::<ChallengeDigest, _, _>(
|
||||
std::iter::once(g1.to_bytes().as_ref())
|
||||
.chain(std::iter::once(instance.to_bytes().as_ref()))
|
||||
.chain(std::iter::once(zkcm_kappa.to_bytes().as_ref()))
|
||||
.chain(std::iter::once(zkcm_phi0.to_bytes().as_ref()))
|
||||
.chain(std::iter::once(zkcm_phi1.to_bytes().as_ref()))
|
||||
.chain(std::iter::once(zkcm_varphi0.to_bytes().as_ref()))
|
||||
.chain(std::iter::once(zkcm_varphi1.to_bytes().as_ref()))
|
||||
// .chain(std::iter::once(zkcm_pg_eq1.to_compressed().as_ref()))
|
||||
// .chain(std::iter::once(zkcm_pg_eq2.to_compressed().as_ref()))
|
||||
// .chain(std::iter::once(zkcm_pg_eq3.to_compressed().as_ref()))
|
||||
.chain(std::iter::once(zkcm_pg_eq4.to_compressed().as_ref()))
|
||||
);
|
||||
|
||||
// compute response for each witness
|
||||
let response_r = produce_response(&r_r, &challenge, &witness.r);
|
||||
let response_r_sk_u = produce_response(&r_sk_u, &challenge, &witness.sk_u.sk);
|
||||
let response_r_v = produce_response(&r_v, &challenge, &witness.v);
|
||||
let response_r_r = produce_response(&r_r, &challenge, &witness.r);
|
||||
let response_r_r1 = produce_response(&r_r1, &challenge, &witness.r1);
|
||||
let response_r_r2 = produce_response(&r_r2, &challenge, &witness.r2);
|
||||
let response_r_varsig1 = produce_response(&r_r_varsig1, &challenge, &witness.r_varsig1);
|
||||
let response_r_theta1 = produce_response(&r_r_theta1, &challenge, &witness.r_theta1);
|
||||
let response_r_varsig2 = produce_response(&r_r_varsig2, &challenge, &witness.r_varsig2);
|
||||
let response_r_theta2 = produce_response(&r_r_theta2, &challenge, &witness.r_theta2);
|
||||
let response_r_rr = produce_response(&r_r_rr, &challenge, &witness.r_rr);
|
||||
let response_r_ss = produce_response(&r_r_ss, &challenge, &witness.r_ss);
|
||||
let response_r_tt = produce_response(&r_r_tt, &challenge, &witness.r_tt);
|
||||
let response_r_rho1 = produce_response(&r_rho1, &challenge, &witness.rho1);
|
||||
let response_r_rho2 = produce_response(&r_rho2, &challenge, &witness.rho2);
|
||||
let response_r_rho3 = produce_response(&r_rho3, &challenge, &witness.rho3);
|
||||
|
||||
|
||||
SpendProof {
|
||||
challenge,
|
||||
response_r,
|
||||
response_r_sk_u,
|
||||
response_r_v,
|
||||
response_r_r,
|
||||
response_r_r1,
|
||||
response_r_r2,
|
||||
response_r_varsig1,
|
||||
response_r_theta1,
|
||||
response_r_varsig2,
|
||||
response_r_theta2,
|
||||
response_r_rr,
|
||||
response_r_ss,
|
||||
response_r_tt,
|
||||
response_r_rho1,
|
||||
response_r_rho2,
|
||||
response_r_rho3,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verify(
|
||||
&self,
|
||||
params: &Parameters,
|
||||
instance: &SpendInstance,
|
||||
verification_key: &VerificationKeyAuth,
|
||||
vv: u64,
|
||||
) -> bool {
|
||||
let grp = params.get_grp();
|
||||
let params_u = params.get_params_u();
|
||||
let params_a = params.get_params_a();
|
||||
let g1 = grp.gen1();
|
||||
|
||||
// re-compute each zkp commitment
|
||||
let zkcm_kappa = instance.kappa * self.challenge
|
||||
+ grp.gen2() * self.response_r
|
||||
+ verification_key.alpha * (Scalar::one() - self.challenge)
|
||||
+ [self.response_r_sk_u, self.response_r_v]
|
||||
.iter()
|
||||
.zip(verification_key.beta_g2.iter())
|
||||
.map(|(attr, beta_i)| beta_i * attr)
|
||||
.sum::<G2Projective>();
|
||||
|
||||
let zkcm_phi0 = g1 * self.response_r_r1 + instance.phi.0 * self.challenge;
|
||||
let zkcm_phi1 = instance.varsig_prime1 * self.response_r_v
|
||||
+ instance.psi_g1 * self.response_r_rho1
|
||||
+ params_u.get_ith_eta(vv as usize) * self.response_r_r1
|
||||
+ instance.phi.1 * self.challenge;
|
||||
let zkcm_varphi0 = g1 * self.response_r_r2 + instance.varphi.0 * self.challenge;
|
||||
let zkcm_varphi1 = (g1 * instance.rr) * self.response_r_sk_u
|
||||
+ instance.theta_prime1 * self.response_r_v
|
||||
+ instance.psi_g1 * self.response_r_rho2
|
||||
+ params_u.get_ith_eta(vv as usize) * self.response_r_r2
|
||||
+ instance.varphi.1 * self.challenge;
|
||||
let zkcm_pg_eq1 = instance.pg_psi0_delta * self.response_r_varsig1
|
||||
+ instance.pg_psi0_gen2 * self.response_r_varsig2.neg()
|
||||
+ instance.pg_eq1 * self.challenge;
|
||||
let zkcm_pg_eq2 = instance.pg_psi0_delta * self.response_r_theta1
|
||||
+ instance.pg_psi0_gen2 * self.response_r_theta2.neg()
|
||||
+ instance.pg_eq2 * self.challenge;
|
||||
|
||||
|
||||
let zkcm_pg_eq3 = instance.pg_psi0_yy * self.response_r_rr
|
||||
+ instance.pg_psi0_gen2 * self.response_r_ss
|
||||
+ instance.pg_psi0_ww1 * self.response_r_varsig2
|
||||
+ instance.pg_psi0_ww2 * self.response_r_theta2
|
||||
+ instance.pg_eq3 * self.challenge;
|
||||
|
||||
let zkcm_pg_eq4 = instance.pg_rr_psi1 * self.response_r_tt
|
||||
+ instance.pg_psi0_tt * self.response_r_rr
|
||||
+ instance.pg_psi0_psi1 * self.response_r_rho3.neg()
|
||||
+ instance.pg_eq4 * self.challenge;
|
||||
|
||||
// re-compute the challenge
|
||||
let challenge = compute_challenge::<ChallengeDigest, _, _>(
|
||||
std::iter::once(g1.to_bytes().as_ref())
|
||||
.chain(std::iter::once(instance.to_bytes().as_ref()))
|
||||
.chain(std::iter::once(zkcm_kappa.to_bytes().as_ref()))
|
||||
.chain(std::iter::once(zkcm_phi0.to_bytes().as_ref()))
|
||||
.chain(std::iter::once(zkcm_phi1.to_bytes().as_ref()))
|
||||
.chain(std::iter::once(zkcm_varphi0.to_bytes().as_ref()))
|
||||
.chain(std::iter::once(zkcm_varphi1.to_bytes().as_ref()))
|
||||
// .chain(std::iter::once(zkcm_pg_eq1.to_compressed().as_ref()))
|
||||
// .chain(std::iter::once(zkcm_pg_eq2.to_compressed().as_ref()))
|
||||
// .chain(std::iter::once(zkcm_pg_eq3.to_compressed().as_ref()))
|
||||
.chain(std::iter::once(zkcm_pg_eq4.to_compressed().as_ref()))
|
||||
);
|
||||
|
||||
challenge == self.challenge
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::ops::Neg;
|
||||
|
||||
use bls12_381::{G2Projective, pairing};
|
||||
use group::Curve;
|
||||
use rand::thread_rng;
|
||||
|
||||
use crate::proofs::proof_spend::{SpendInstance, SpendProof, SpendWitness};
|
||||
use crate::scheme::{PayInfo, Phi, VarPhi};
|
||||
use crate::scheme::aggregation::aggregate_verification_keys;
|
||||
use crate::scheme::keygen::{PublicKeyUser, SecretKeyUser, ttp_keygen_authorities, VerificationKeyAuth};
|
||||
use crate::scheme::setup::{GroupParameters, Parameters};
|
||||
use crate::utils::hash_to_scalar;
|
||||
|
||||
#[test]
|
||||
fn spend_proof_construct_and_verify() {
|
||||
let rng = thread_rng();
|
||||
let grp = GroupParameters::new().unwrap();
|
||||
let params = Parameters::new(grp.clone());
|
||||
let params_u = params.get_params_u();
|
||||
let params_a = params.get_params_a();
|
||||
|
||||
let sk = grp.random_scalar();
|
||||
let pk_user = PublicKeyUser {
|
||||
pk: grp.gen1() * sk,
|
||||
};
|
||||
let v = grp.random_scalar();
|
||||
let attributes = vec![sk, v];
|
||||
let l: usize = 10;
|
||||
let vv: u64 = 20;
|
||||
|
||||
let authorities_keypairs = ttp_keygen_authorities(¶ms, 2, 3).unwrap();
|
||||
let verification_keys_auth: Vec<VerificationKeyAuth> = authorities_keypairs
|
||||
.iter()
|
||||
.map(|keypair| keypair.verification_key())
|
||||
.collect();
|
||||
|
||||
let verification_key =
|
||||
aggregate_verification_keys(&verification_keys_auth, Some(&[1, 2, 3])).unwrap();
|
||||
|
||||
let r = grp.random_scalar();
|
||||
let kappa = grp.gen2() * r
|
||||
+ verification_key.alpha
|
||||
+ attributes
|
||||
.iter()
|
||||
.zip(verification_key.beta_g2.iter())
|
||||
.map(|(priv_attr, beta_i)| beta_i * priv_attr)
|
||||
.sum::<G2Projective>();
|
||||
|
||||
let r1 = grp.random_scalar();
|
||||
let r2 = grp.random_scalar();
|
||||
let phi = Phi(grp.gen1() * r1, params_u.get_ith_sigma(l as usize) * v + params_u.get_ith_eta(vv as usize) * r1);
|
||||
|
||||
let pay_info = PayInfo { info: [78u8; 32] };
|
||||
let rr = hash_to_scalar(pay_info.info);
|
||||
let varphi = VarPhi(grp.gen1() * r2, (grp.gen1() * rr) * sk + params_u.get_ith_theta(l as usize) * v + params_u.get_ith_eta(vv as usize) * r2);
|
||||
|
||||
// random value used to compute blinded bases
|
||||
let r_varsig1 = grp.random_scalar();
|
||||
let r_theta1 = grp.random_scalar();
|
||||
let r_varsig2 = grp.random_scalar();
|
||||
let r_theta2 = grp.random_scalar();
|
||||
let r_rr = grp.random_scalar();
|
||||
let r_ss = grp.random_scalar();
|
||||
let r_tt = grp.random_scalar();
|
||||
|
||||
|
||||
// compute blinded bases
|
||||
let psi_g1 = params_u.get_psi_g1();
|
||||
let psi_g2 = params_u.get_psi_g2();
|
||||
let varsig_prime1 = params_u.get_ith_sigma(l as usize) + (psi_g1 * r_varsig1);
|
||||
let theta_prime1 = params_u.get_ith_theta(l as usize) + (psi_g1 * r_theta1);
|
||||
let varsig_prime2 = params_u.get_ith_sigma(l as usize + vv as usize - 1) + (psi_g1 * r_varsig2);
|
||||
let theta_prime2 = params_u.get_ith_sigma(l as usize + vv as usize - 1) + (psi_g1 * r_theta2);
|
||||
let rr_prime = params_u.get_ith_sps_sign(l as usize + vv as usize - 1).rr + (psi_g1 * r_rr);
|
||||
let ss_prime = params_u.get_ith_sps_sign(l as usize + vv as usize - 1).ss + (psi_g1 * r_ss);
|
||||
let tt_prime = params_u.get_ith_sps_sign(l as usize + vv as usize - 1).tt + (psi_g2 * r_tt);
|
||||
|
||||
let rho1 = v.neg() * r_varsig1;
|
||||
let rho2 = v.neg() * r_theta1;
|
||||
let rho3 = r_rr * r_tt;
|
||||
|
||||
let pg_varsigpr1_delta = pairing(&varsig_prime1.to_affine(), ¶ms_a.get_ith_delta((vv - 1) as usize).to_affine());
|
||||
let pg_psi0_delta = pairing(&psi_g1.to_affine(), ¶ms_a.get_ith_delta((vv - 1) as usize).to_affine());
|
||||
let pg_varsigpr2_gen2 = pairing(&varsig_prime2.to_affine(), grp.gen2());
|
||||
let pg_psi0_gen2 = pairing(&psi_g1.to_affine(), grp.gen2());
|
||||
let pg_thetapr1_delta = pairing(&theta_prime1.to_affine(), ¶ms_a.get_ith_delta((vv - 1) as usize).to_affine());
|
||||
let pg_thetapr2_gen2 = pairing(&theta_prime2.to_affine(), grp.gen2());
|
||||
let yy = params_u.get_sps_pk().get_yy();
|
||||
let pg_rrprime_yy = pairing(&rr_prime.to_affine(), &yy.to_affine());
|
||||
let pg_psi0_yy = pairing(&psi_g1.to_affine(), &yy.to_affine());
|
||||
let pg_ssprime_gen2 = pairing(&ss_prime.to_affine(), grp.gen2());
|
||||
let ww1 = params_u.get_sps_pk().get_ith_ww(0);
|
||||
let ww2 = params_u.get_sps_pk().get_ith_ww(1);
|
||||
let pg_varsigpr2_ww1 = pairing(&varsig_prime2.to_affine(), &ww1.to_affine());
|
||||
let pg_psi0_ww1 = pairing(&psi_g1.to_affine(), &ww1.to_affine());
|
||||
let pg_thetapr2_ww2 = pairing(&theta_prime1.to_affine(), &ww2.to_affine());
|
||||
let pg_psi0_ww2 = pairing(&psi_g1.to_affine(), &ww2.to_affine());
|
||||
let pg_gen1_zz = pairing(grp.gen1(), ¶ms_u.get_sps_pk().get_zz().to_affine());
|
||||
let pg_rr_tt = pairing(&rr_prime.to_affine(), &tt_prime.to_affine());
|
||||
let pg_rr_psi1 = pairing(&rr_prime.to_affine(), &psi_g2.to_affine());
|
||||
let pg_psi0_tt = pairing(&psi_g1.to_affine(), &tt_prime.to_affine());
|
||||
let pg_psi0_psi1 = pairing(&psi_g1.to_affine(), &psi_g2.to_affine());
|
||||
let pg_gen1_gen2 = pairing(grp.gen1(), grp.gen2());
|
||||
|
||||
let pg_eq1 = pg_varsigpr1_delta - pg_varsigpr2_gen2;
|
||||
let pg_eq2 = pg_thetapr1_delta - pg_thetapr2_gen2;
|
||||
let pg_eq3 = pg_rrprime_yy + pg_ssprime_gen2 + pg_varsigpr2_ww1 + pg_thetapr2_ww2 + pg_gen1_zz.neg();
|
||||
let pg_eq4 = pg_rr_tt - pg_gen1_gen2;
|
||||
|
||||
let instance = SpendInstance {
|
||||
kappa,
|
||||
phi,
|
||||
varphi,
|
||||
rr,
|
||||
rr_prime,
|
||||
ss: ss_prime,
|
||||
tt: tt_prime,
|
||||
varsig_prime1,
|
||||
theta_prime1,
|
||||
pg_eq1,
|
||||
pg_eq2,
|
||||
pg_eq3,
|
||||
pg_eq4,
|
||||
psi_g1: *psi_g1,
|
||||
psi_g2: *psi_g2,
|
||||
pg_psi0_delta,
|
||||
pg_psi0_gen2,
|
||||
pg_psi0_yy,
|
||||
pg_psi0_ww1,
|
||||
pg_psi0_ww2,
|
||||
pg_rr_psi1,
|
||||
pg_psi0_tt,
|
||||
pg_psi0_psi1,
|
||||
};
|
||||
|
||||
let witness = SpendWitness {
|
||||
sk_u: SecretKeyUser { sk },
|
||||
v,
|
||||
r,
|
||||
r1,
|
||||
r2,
|
||||
r_varsig1,
|
||||
r_theta1,
|
||||
r_varsig2,
|
||||
r_theta2,
|
||||
r_rr,
|
||||
r_ss,
|
||||
r_tt,
|
||||
rho1,
|
||||
rho2,
|
||||
rho3,
|
||||
};
|
||||
|
||||
// compute the zk proof
|
||||
let zk_proof = SpendProof::construct(¶ms, &instance, &witness, &verification_key, vv);
|
||||
assert!(zk_proof.verify(¶ms, &instance, &verification_key, vv))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use std::cell::Cell;
|
||||
use std::ops::Neg;
|
||||
|
||||
use bls12_381::{G1Projective, G2Prepared, G2Projective, pairing, Scalar};
|
||||
use group::Curve;
|
||||
use group::{Curve, GroupEncoding};
|
||||
|
||||
use crate::Attribute;
|
||||
use crate::constants::L;
|
||||
@@ -36,9 +37,26 @@ pub fn compute_kappa(
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Phi(pub(crate) G1Projective, pub(crate) G1Projective);
|
||||
|
||||
impl Phi {
|
||||
pub(crate) fn to_bytes(&self) -> Vec<u8> {
|
||||
let mut bytes = Vec::with_capacity(48 + 48);
|
||||
bytes.extend_from_slice(self.0.to_bytes().as_ref());
|
||||
bytes.extend_from_slice(self.1.to_bytes().as_ref());
|
||||
bytes
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct VarPhi(pub(crate) G1Projective, pub(crate) G1Projective);
|
||||
|
||||
impl VarPhi {
|
||||
pub(crate) fn to_bytes(&self) -> Vec<u8> {
|
||||
let mut bytes = Vec::with_capacity(48 + 48);
|
||||
bytes.extend_from_slice(self.0.to_bytes().as_ref());
|
||||
bytes.extend_from_slice(self.1.to_bytes().as_ref());
|
||||
bytes
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PayInfo {
|
||||
pub info: [u8; 32],
|
||||
@@ -51,7 +69,7 @@ pub struct Payment {
|
||||
phi: Phi,
|
||||
varphi: VarPhi,
|
||||
rr: Scalar,
|
||||
zkproof: SpendProof,
|
||||
zk_proof: SpendProof,
|
||||
vv: u64,
|
||||
}
|
||||
|
||||
@@ -135,69 +153,72 @@ impl Wallet {
|
||||
let r_tt = grp.random_scalar();
|
||||
|
||||
// compute blinded bases
|
||||
let psi0 = params_u.get_psi0();
|
||||
let psi1 = params_u.get_psi1();
|
||||
let varsig_prime1 = params_u.get_ith_sigma(self.l() as usize) + (psi0 * r_varsig1);
|
||||
let theta_prime1 = params_u.get_ith_theta(self.l() as usize) + (psi0 * r_theta1);
|
||||
let varsig_prime2 = params_u.get_ith_sigma(self.l() as usize + vv as usize - 1) + (psi0 * r_varsig2);
|
||||
let theta_prime2 = params_u.get_ith_sigma(self.l() as usize + vv as usize - 1) + (psi0 * r_theta2);
|
||||
let rr_prime = params_u.get_ith_sps_sign(self.l() as usize + vv as usize - 1).rr + (psi0 * r_rr);
|
||||
let ss_prime = params_u.get_ith_sps_sign(self.l() as usize + vv as usize - 1).ss + (psi0 * r_ss);
|
||||
let tt_prime = params_u.get_ith_sps_sign(self.l() as usize + vv as usize - 1).tt + (psi1 * r_tt);
|
||||
let psi_g1 = params_u.get_psi_g1();
|
||||
let psi_g2 = params_u.get_psi_g2();
|
||||
let varsig_prime1 = params_u.get_ith_sigma(self.l() as usize) + (psi_g1 * r_varsig1);
|
||||
let theta_prime1 = params_u.get_ith_theta(self.l() as usize) + (psi_g1 * r_theta1);
|
||||
let varsig_prime2 = params_u.get_ith_sigma(self.l() as usize + vv as usize - 1) + (psi_g1 * r_varsig2);
|
||||
let theta_prime2 = params_u.get_ith_sigma(self.l() as usize + vv as usize - 1) + (psi_g1 * r_theta2);
|
||||
let rr_prime = params_u.get_ith_sps_sign(self.l() as usize + vv as usize - 1).rr + (psi_g1 * r_rr);
|
||||
let ss_prime = params_u.get_ith_sps_sign(self.l() as usize + vv as usize - 1).ss + (psi_g1 * r_ss);
|
||||
let tt_prime = params_u.get_ith_sps_sign(self.l() as usize + vv as usize - 1).tt + (psi_g2 * r_tt);
|
||||
|
||||
let rho1 = self.v.neg() * r_varsig1;
|
||||
let rho2 = self.v.neg() * r_theta1;
|
||||
let rho3 = r_rr * r_tt;
|
||||
|
||||
let pg_varsigpr1_delta = pairing(&varsig_prime1.to_affine(), ¶ms_a.get_ith_delta((vv - 1) as usize).to_affine());
|
||||
let pg_psi0_delta = pairing(&psi0.to_affine(), ¶ms_a.get_ith_delta((vv - 1) as usize).to_affine());
|
||||
let pg_psi0_delta = pairing(&psi_g1.to_affine(), ¶ms_a.get_ith_delta((vv - 1) as usize).to_affine());
|
||||
let pg_varsigpr2_gen2 = pairing(&varsig_prime2.to_affine(), grp.gen2());
|
||||
let pg_psi0_gen2 = pairing(&psi0.to_affine(), grp.gen2());
|
||||
let pg_psi0_gen2 = pairing(&psi_g1.to_affine(), grp.gen2());
|
||||
let pg_thetapr1_delta = pairing(&theta_prime1.to_affine(), ¶ms_a.get_ith_delta((vv - 1) as usize).to_affine());
|
||||
let pg_thetapr2_gen2 = pairing(&theta_prime1.to_affine(), grp.gen2());
|
||||
let pg_thetapr2_gen2 = pairing(&theta_prime2.to_affine(), grp.gen2());
|
||||
let yy = params_u.get_sps_pk().get_yy();
|
||||
let pg_rr_yy = pairing(&rr_prime.to_affine(), &yy.to_affine());
|
||||
let pg_psi0_yy = pairing(&psi0.to_affine(), &yy.to_affine());
|
||||
let pg_rrprime_yy = pairing(&rr_prime.to_affine(), &yy.to_affine());
|
||||
let pg_psi0_yy = pairing(&psi_g1.to_affine(), &yy.to_affine());
|
||||
let pg_ssprime_gen2 = pairing(&ss_prime.to_affine(), grp.gen2());
|
||||
let ww1 = params_u.get_sps_pk().get_ith_ww(0);
|
||||
let ww2 = params_u.get_sps_pk().get_ith_ww(1);
|
||||
let pg_varsigpr2_ww1 = pairing(&varsig_prime2.to_affine(), &ww1.to_affine());
|
||||
let pg_psi0_ww1 = pairing(&psi0.to_affine(), &ww1.to_affine());
|
||||
let pg_psi0_ww1 = pairing(&psi_g1.to_affine(), &ww1.to_affine());
|
||||
let pg_thetapr2_ww2 = pairing(&theta_prime1.to_affine(), &ww2.to_affine());
|
||||
let pg_psi0_ww2 = pairing(&psi0.to_affine(), &ww2.to_affine());
|
||||
let pg_psi0_ww2 = pairing(&psi_g1.to_affine(), &ww2.to_affine());
|
||||
let pg_gen1_zz = pairing(grp.gen1(), ¶ms_u.get_sps_pk().get_zz().to_affine());
|
||||
let pg_rr_tt = pairing(&rr_prime.to_affine(), &tt_prime.to_affine());
|
||||
let pg_rr_psi1 = pairing(&rr_prime.to_affine(), &psi1.to_affine());
|
||||
let pg_psi0_tt = pairing(&psi0.to_affine(), &tt_prime.to_affine());
|
||||
let pg_psi0_psi1 = pairing(&psi0.to_affine(), &psi1.to_affine());
|
||||
let pg_rr_psi1 = pairing(&rr_prime.to_affine(), &psi_g2.to_affine());
|
||||
let pg_psi0_tt = pairing(&psi_g1.to_affine(), &tt_prime.to_affine());
|
||||
let pg_psi0_psi1 = pairing(&psi_g1.to_affine(), &psi_g2.to_affine());
|
||||
let pg_gen1_gen2 = pairing(grp.gen1(), grp.gen2());
|
||||
|
||||
let pg_eq1 = pg_varsigpr1_delta - pg_varsigpr2_gen2;
|
||||
let pg_eq2 = pg_thetapr1_delta - pg_thetapr2_gen2;
|
||||
let pg_eq3 = pg_rrprime_yy + pg_ssprime_gen2 + pg_varsigpr2_ww1 + pg_thetapr2_ww2 + pg_gen1_zz.neg();
|
||||
let pg_eq4 = pg_rr_tt - pg_gen1_gen2;
|
||||
|
||||
let instance = SpendInstance {
|
||||
kappa,
|
||||
phi,
|
||||
varphi,
|
||||
rr: rr_prime,
|
||||
rr,
|
||||
rr_prime,
|
||||
ss: ss_prime,
|
||||
tt: tt_prime,
|
||||
pg_varsigpr1_delta,
|
||||
varsig_prime1,
|
||||
theta_prime1,
|
||||
pg_eq1,
|
||||
pg_eq2,
|
||||
pg_eq3,
|
||||
pg_eq4,
|
||||
psi_g1: *psi_g1,
|
||||
psi_g2: *psi_g2,
|
||||
pg_psi0_delta,
|
||||
pg_varsigpr2_gen2,
|
||||
pg_psi0_gen2,
|
||||
pg_thetapr1_delta,
|
||||
pg_thetapr2_gen2,
|
||||
pg_rr_yy,
|
||||
pg_psi0_yy,
|
||||
pg_ssprime_gen2,
|
||||
pg_varsigpr2_ww1,
|
||||
pg_psi0_ww1,
|
||||
pg_thetapr2_ww2,
|
||||
pg_psi0_ww2,
|
||||
pg_gen1_zz,
|
||||
pg_rr_tt,
|
||||
pg_rr_psi1,
|
||||
pg_psi0_tt,
|
||||
pg_psi0_psi1,
|
||||
pg_gen1_gen2,
|
||||
};
|
||||
|
||||
let witness = SpendWitness {
|
||||
@@ -219,7 +240,7 @@ impl Wallet {
|
||||
};
|
||||
|
||||
// compute the zk proof
|
||||
let zkproof = SpendProof::construct(params, &instance, &witness);
|
||||
let zk_proof = SpendProof::construct(params, &instance, &witness, &verification_key, vv);
|
||||
|
||||
// output pay and updated wallet
|
||||
let pay = Payment {
|
||||
@@ -228,7 +249,7 @@ impl Wallet {
|
||||
phi,
|
||||
varphi,
|
||||
rr,
|
||||
zkproof,
|
||||
zk_proof,
|
||||
vv,
|
||||
};
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ pub struct GroupParameters {
|
||||
_g2_prepared_miller: G2Prepared,
|
||||
}
|
||||
|
||||
|
||||
impl GroupParameters {
|
||||
pub fn new() -> Result<GroupParameters> {
|
||||
Ok(GroupParameters {
|
||||
@@ -66,8 +67,9 @@ impl Parameters {
|
||||
pub fn new(grp: GroupParameters) -> Parameters {
|
||||
let g1 = grp.gen1();
|
||||
let g2 = grp.gen2();
|
||||
let psi1 = hash_g1("psi1");
|
||||
let psi2 = hash_g2("psi2");
|
||||
|
||||
let psi_g1 = hash_g1("psi1");
|
||||
let psi_g2 = hash_g2("psi2");
|
||||
let gamma1 = hash_g1("gamma1");
|
||||
let gamma2 = hash_g1("gamma2");
|
||||
let eta = hash_g1("eta");
|
||||
@@ -96,9 +98,7 @@ impl Parameters {
|
||||
|
||||
let mut etas_a: Vec<G2Projective> = Default::default();
|
||||
for l in 1..=L {
|
||||
println!("l = {:?}", l);
|
||||
for k in 0..=l - 1 {
|
||||
println!("k = {:?}", k);
|
||||
etas_a.push(g2 * (vec_a[l as usize - 1].neg() * (y * Scalar::from(k))));
|
||||
}
|
||||
}
|
||||
@@ -115,8 +115,8 @@ impl Parameters {
|
||||
// Compute signature for each pair sigma, theta
|
||||
let params_u = ParametersUser {
|
||||
gammas: vec![gamma1, gamma2],
|
||||
psi1,
|
||||
psi2,
|
||||
psi_g1,
|
||||
psi_g2,
|
||||
eta,
|
||||
omega,
|
||||
etas: etas_u,
|
||||
@@ -142,8 +142,8 @@ impl Parameters {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ParametersUser {
|
||||
gammas: Vec<G1Projective>,
|
||||
psi1: G1Projective,
|
||||
psi2: G2Projective,
|
||||
psi_g1: G1Projective,
|
||||
psi_g2: G2Projective,
|
||||
eta: G1Projective,
|
||||
omega: G1Projective,
|
||||
etas: Vec<G1Projective>,
|
||||
@@ -156,9 +156,9 @@ pub struct ParametersUser {
|
||||
impl ParametersUser {
|
||||
pub(crate) fn get_gammas(&self) -> &Vec<G1Projective> { &self.gammas }
|
||||
|
||||
pub(crate) fn get_psi0(&self) -> &G1Projective { &self.psi1 }
|
||||
pub(crate) fn get_psi_g1(&self) -> &G1Projective { &self.psi_g1 }
|
||||
|
||||
pub(crate) fn get_psi1(&self) -> &G2Projective { &self.psi2 }
|
||||
pub(crate) fn get_psi_g2(&self) -> &G2Projective { &self.psi_g2 }
|
||||
|
||||
pub(crate) fn get_eta(&self) -> &G1Projective { &self.eta }
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ impl Polynomial {
|
||||
Scalar::zero()
|
||||
// if x is zero then we can ignore most of the expensive computation and
|
||||
// just return the last term of the polynomial
|
||||
} else if x.is_zero() {
|
||||
} else if x.is_zero().unwrap_u8() == 1 {
|
||||
// we checked that coefficients are not empty so unwrap here is fine
|
||||
*self.coefficients.first().unwrap()
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user