From 7543e6552f2159a927f89bc7d1c803e7f512787f Mon Sep 17 00:00:00 2001 From: Domas Kalinauskas Date: Fri, 1 Dec 2023 16:13:48 +0200 Subject: [PATCH 01/17] init commit --- Cargo.lock | 24 + Cargo.toml | 1 + constantine/Cargo.toml | 76 + constantine/benches/das.rs | 16 + constantine/benches/eip_4844.rs | 28 + constantine/benches/fft.rs | 20 + constantine/benches/fk_20.rs | 28 + constantine/benches/kzg.rs | 28 + constantine/benches/lincomb.rs | 16 + constantine/benches/poly.rs | 16 + constantine/benches/recover.rs | 18 + constantine/benches/zero_poly.rs | 18 + constantine/bls12_381/Cargo.toml | 73 + constantine/bls12_381/benches/groups.rs | 170 ++ .../bls12_381/benches/hash_to_curve.rs | 68 + constantine/bls12_381/src/fp.rs | 990 ++++++++ constantine/bls12_381/src/fp12.rs | 659 +++++ constantine/bls12_381/src/fp2.rs | 898 +++++++ constantine/bls12_381/src/fp6.rs | 571 +++++ constantine/bls12_381/src/g1.rs | 1798 ++++++++++++++ constantine/bls12_381/src/g2.rs | 2180 +++++++++++++++++ .../bls12_381/src/hash_to_curve/chain.rs | 920 +++++++ .../bls12_381/src/hash_to_curve/expand_msg.rs | 1282 ++++++++++ .../bls12_381/src/hash_to_curve/map_g1.rs | 961 ++++++++ .../bls12_381/src/hash_to_curve/map_g2.rs | 900 +++++++ .../bls12_381/src/hash_to_curve/map_scalar.rs | 39 + .../bls12_381/src/hash_to_curve/mod.rs | 113 + constantine/bls12_381/src/lib.rs | 97 + constantine/bls12_381/src/notes/design.rs | 108 + .../bls12_381/src/notes/serialization.rs | 29 + constantine/bls12_381/src/pairings.rs | 970 ++++++++ constantine/bls12_381/src/scalar.rs | 1278 ++++++++++ .../g1_compressed_valid_test_vectors.dat | Bin 0 -> 48000 bytes .../g1_uncompressed_valid_test_vectors.dat | Bin 0 -> 96000 bytes .../g2_compressed_valid_test_vectors.dat | Bin 0 -> 96000 bytes .../g2_uncompressed_valid_test_vectors.dat | Bin 0 -> 192000 bytes constantine/bls12_381/src/tests/mod.rs | 231 ++ constantine/bls12_381/src/util.rs | 174 ++ constantine/csharp.patch | 25 + constantine/go.patch | 43 + constantine/java.patch | 24 + constantine/nodejs.patch | 74 + constantine/python.patch | 48 + constantine/rust.patch | 71 + constantine/src/consts.rs | 214 ++ constantine/src/das.rs | 87 + constantine/src/eip_4844.rs | 406 +++ constantine/src/fft.rs | 106 + constantine/src/fft_g1.rs | 122 + constantine/src/fk20_proofs.rs | 324 +++ constantine/src/kzg_proofs.rs | 105 + constantine/src/kzg_types.rs | 745 ++++++ constantine/src/lib.rs | 53 + constantine/src/multiscalar_mul.rs | 194 ++ constantine/src/poly.rs | 404 +++ constantine/src/recover.rs | 233 ++ constantine/src/utils.rs | 58 + constantine/src/zero_poly.rs | 197 ++ constantine/tests/bls12_381.rs | 114 + constantine/tests/consts.rs | 36 + constantine/tests/das.rs | 16 + constantine/tests/eip_4844.rs | 247 ++ constantine/tests/fft_fr.rs | 27 + constantine/tests/fft_g1.rs | 22 + constantine/tests/fk20_proofs.rs | 82 + constantine/tests/kzg_proofs.rs | 31 + constantine/tests/poly.rs | 82 + constantine/tests/recover.rs | 23 + constantine/tests/zero_poly.rs | 45 + 69 files changed, 19056 insertions(+) create mode 100644 constantine/Cargo.toml create mode 100644 constantine/benches/das.rs create mode 100644 constantine/benches/eip_4844.rs create mode 100644 constantine/benches/fft.rs create mode 100644 constantine/benches/fk_20.rs create mode 100644 constantine/benches/kzg.rs create mode 100644 constantine/benches/lincomb.rs create mode 100644 constantine/benches/poly.rs create mode 100644 constantine/benches/recover.rs create mode 100644 constantine/benches/zero_poly.rs create mode 100644 constantine/bls12_381/Cargo.toml create mode 100644 constantine/bls12_381/benches/groups.rs create mode 100644 constantine/bls12_381/benches/hash_to_curve.rs create mode 100644 constantine/bls12_381/src/fp.rs create mode 100644 constantine/bls12_381/src/fp12.rs create mode 100644 constantine/bls12_381/src/fp2.rs create mode 100644 constantine/bls12_381/src/fp6.rs create mode 100644 constantine/bls12_381/src/g1.rs create mode 100644 constantine/bls12_381/src/g2.rs create mode 100644 constantine/bls12_381/src/hash_to_curve/chain.rs create mode 100644 constantine/bls12_381/src/hash_to_curve/expand_msg.rs create mode 100644 constantine/bls12_381/src/hash_to_curve/map_g1.rs create mode 100644 constantine/bls12_381/src/hash_to_curve/map_g2.rs create mode 100644 constantine/bls12_381/src/hash_to_curve/map_scalar.rs create mode 100644 constantine/bls12_381/src/hash_to_curve/mod.rs create mode 100644 constantine/bls12_381/src/lib.rs create mode 100644 constantine/bls12_381/src/notes/design.rs create mode 100644 constantine/bls12_381/src/notes/serialization.rs create mode 100644 constantine/bls12_381/src/pairings.rs create mode 100644 constantine/bls12_381/src/scalar.rs create mode 100644 constantine/bls12_381/src/tests/g1_compressed_valid_test_vectors.dat create mode 100644 constantine/bls12_381/src/tests/g1_uncompressed_valid_test_vectors.dat create mode 100644 constantine/bls12_381/src/tests/g2_compressed_valid_test_vectors.dat create mode 100644 constantine/bls12_381/src/tests/g2_uncompressed_valid_test_vectors.dat create mode 100644 constantine/bls12_381/src/tests/mod.rs create mode 100644 constantine/bls12_381/src/util.rs create mode 100644 constantine/csharp.patch create mode 100644 constantine/go.patch create mode 100644 constantine/java.patch create mode 100644 constantine/nodejs.patch create mode 100644 constantine/python.patch create mode 100644 constantine/rust.patch create mode 100644 constantine/src/consts.rs create mode 100644 constantine/src/das.rs create mode 100644 constantine/src/eip_4844.rs create mode 100644 constantine/src/fft.rs create mode 100644 constantine/src/fft_g1.rs create mode 100644 constantine/src/fk20_proofs.rs create mode 100644 constantine/src/kzg_proofs.rs create mode 100644 constantine/src/kzg_types.rs create mode 100644 constantine/src/lib.rs create mode 100644 constantine/src/multiscalar_mul.rs create mode 100644 constantine/src/poly.rs create mode 100644 constantine/src/recover.rs create mode 100644 constantine/src/utils.rs create mode 100644 constantine/src/zero_poly.rs create mode 100644 constantine/tests/bls12_381.rs create mode 100644 constantine/tests/consts.rs create mode 100644 constantine/tests/das.rs create mode 100644 constantine/tests/eip_4844.rs create mode 100644 constantine/tests/fft_fr.rs create mode 100644 constantine/tests/fft_g1.rs create mode 100644 constantine/tests/fk20_proofs.rs create mode 100644 constantine/tests/kzg_proofs.rs create mode 100644 constantine/tests/poly.rs create mode 100644 constantine/tests/recover.rs create mode 100644 constantine/tests/zero_poly.rs diff --git a/Cargo.lock b/Cargo.lock index d5f04ea01..b6a2f1cdf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -394,6 +394,11 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" +[[package]] +name = "constantine-sys" +version = "0.1.0" +source = "git+https://github.com/mratsim/constantine.git#8367d7d19cdbba874aab961b70d272e742184c37" + [[package]] name = "cpufeatures" version = "0.2.11" @@ -1206,6 +1211,25 @@ dependencies = [ "smallvec", ] +[[package]] +name = "rust-kzg-constantine" +version = "0.1.0" +dependencies = [ + "bls12_381", + "blst", + "byteorder", + "constantine-sys", + "criterion 0.5.1", + "ff", + "hex", + "kzg", + "kzg-bench", + "libc", + "rand", + "rayon", + "subtle", +] + [[package]] name = "rust-kzg-mcl" version = "0.0.1" diff --git a/Cargo.toml b/Cargo.toml index 1c64289aa..38ae210d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "mcl/kzg", "mcl/kzg-bench", "zkcrypto", + "constantine" ] diff --git a/constantine/Cargo.toml b/constantine/Cargo.toml new file mode 100644 index 000000000..51287c504 --- /dev/null +++ b/constantine/Cargo.toml @@ -0,0 +1,76 @@ +[package] +name = "rust-kzg-constantine" +version = "0.1.0" +edition = "2021" + +[dependencies] +blst = {'git' = 'https://github.com/supranational/blst.git'} +kzg = { path = "../kzg", default-features = false } +constantine-sys = { 'git' = 'https://github.com/mratsim/constantine.git' } +bls12_381 = { path = "../zkcrypto/bls12_381" } +ff = { version = "0.13", features = ["derive"] } +hex = "0.4.3" +rand = { version = "0.8.5", optional = true } +libc = { version = "0.2.148", default-features = false } +rayon = { version = "1.8.0", optional = true } +subtle = "2.5.0" +byteorder = "1.5.0" + +[dev-dependencies] +criterion = "0.5.1" +kzg-bench = { path = "../kzg-bench" } +rand = { version = "0.8.5" } + +[features] +default = [ + "std", + "rand" +] +std = [ + "kzg/std", + "libc/std" +] +parallel = [ + "dep:rayon", "kzg/parallel" +] +rand = [ + "dep:rand", + "kzg/rand", +] + +[[bench]] +name = "fft" +harness = false + +[[bench]] +name = "kzg" +harness = false + +[[bench]] +name = "poly" +harness = false + +[[bench]] +name = "das" +harness = false + +[[bench]] +name = "fk_20" +harness = false + +[[bench]] +name = "recover" +harness = false + +[[bench]] +name = "zero_poly" +harness = false + +[[bench]] +name = "eip_4844" +harness = false + +[[bench]] +name = "lincomb" +harness = false + diff --git a/constantine/benches/das.rs b/constantine/benches/das.rs new file mode 100644 index 000000000..725f3e429 --- /dev/null +++ b/constantine/benches/das.rs @@ -0,0 +1,16 @@ +use criterion::{criterion_group, criterion_main, Criterion}; +use kzg_bench::benches::das::bench_das_extension; +use rust_kzg_zkcrypto::kzg_proofs::FFTSettings; +use rust_kzg_zkcrypto::kzg_types::ZFr; + +fn bench_das_extension_(c: &mut Criterion) { + bench_das_extension::(c); +} + +criterion_group! { + name = benches; + config = Criterion::default().sample_size(10); + targets = bench_das_extension_ +} + +criterion_main!(benches); diff --git a/constantine/benches/eip_4844.rs b/constantine/benches/eip_4844.rs new file mode 100644 index 000000000..79d0116f4 --- /dev/null +++ b/constantine/benches/eip_4844.rs @@ -0,0 +1,28 @@ +use criterion::{criterion_group, criterion_main, Criterion}; +use kzg::eip_4844::{ + blob_to_kzg_commitment_rust, bytes_to_blob, compute_blob_kzg_proof_rust, + compute_kzg_proof_rust, verify_blob_kzg_proof_batch_rust, verify_blob_kzg_proof_rust, + verify_kzg_proof_rust, +}; +use kzg_bench::benches::eip_4844::bench_eip_4844; +use rust_kzg_zkcrypto::eip_4844::load_trusted_setup_filename_rust; +use rust_kzg_zkcrypto::kzg_proofs::{FFTSettings, KZGSettings}; +use rust_kzg_zkcrypto::kzg_types::{ZFr, ZG1, ZG2}; +use rust_kzg_zkcrypto::poly::PolyData; + +fn bench_eip_4844_(c: &mut Criterion) { + bench_eip_4844::( + c, + &load_trusted_setup_filename_rust, + &blob_to_kzg_commitment_rust, + &bytes_to_blob, + &compute_kzg_proof_rust, + &verify_kzg_proof_rust, + &compute_blob_kzg_proof_rust, + &verify_blob_kzg_proof_rust, + &verify_blob_kzg_proof_batch_rust, + ); +} + +criterion_group!(benches, bench_eip_4844_,); +criterion_main!(benches); diff --git a/constantine/benches/fft.rs b/constantine/benches/fft.rs new file mode 100644 index 000000000..de1d4cdf1 --- /dev/null +++ b/constantine/benches/fft.rs @@ -0,0 +1,20 @@ +use criterion::{criterion_group, criterion_main, Criterion}; +use kzg_bench::benches::fft::{bench_fft_fr, bench_fft_g1}; +use rust_kzg_zkcrypto::kzg_proofs::FFTSettings; +use rust_kzg_zkcrypto::kzg_types::{ZFr, ZG1}; + +fn bench_fft_fr_(c: &mut Criterion) { + bench_fft_fr::(c); +} + +fn bench_fft_g1_(c: &mut Criterion) { + bench_fft_g1::(c); +} + +criterion_group! { + name = benches; + config = Criterion::default().sample_size(10); + targets = bench_fft_fr_, bench_fft_g1_ +} + +criterion_main!(benches); diff --git a/constantine/benches/fk_20.rs b/constantine/benches/fk_20.rs new file mode 100644 index 000000000..5e1292e36 --- /dev/null +++ b/constantine/benches/fk_20.rs @@ -0,0 +1,28 @@ +use criterion::{criterion_group, criterion_main, Criterion}; +use kzg_bench::benches::fk20::{bench_fk_multi_da, bench_fk_single_da}; +use rust_kzg_zkcrypto::fk20_proofs::{KzgFK20MultiSettings, KzgFK20SingleSettings}; +use rust_kzg_zkcrypto::kzg_proofs::{generate_trusted_setup, FFTSettings, KZGSettings}; +use rust_kzg_zkcrypto::kzg_types::{ZFr, ZG1, ZG2}; +use rust_kzg_zkcrypto::poly::PolyData; + +fn bench_fk_single_da_(c: &mut Criterion) { + bench_fk_single_da::( + c, + &generate_trusted_setup, + ) +} + +fn bench_fk_multi_da_(c: &mut Criterion) { + bench_fk_multi_da::( + c, + &generate_trusted_setup, + ) +} + +criterion_group! { + name = benches; + config = Criterion::default().sample_size(10); + targets = bench_fk_single_da_, bench_fk_multi_da_ +} + +criterion_main!(benches); diff --git a/constantine/benches/kzg.rs b/constantine/benches/kzg.rs new file mode 100644 index 000000000..d8ac61ff8 --- /dev/null +++ b/constantine/benches/kzg.rs @@ -0,0 +1,28 @@ +use criterion::{criterion_group, criterion_main, Criterion}; +use kzg_bench::benches::kzg::{bench_commit_to_poly, bench_compute_proof_single}; + +use rust_kzg_zkcrypto::kzg_proofs::{generate_trusted_setup, FFTSettings, KZGSettings}; +use rust_kzg_zkcrypto::kzg_types::{ZFr, ZG1, ZG2}; +use rust_kzg_zkcrypto::poly::PolyData; + +fn bench_commit_to_poly_(c: &mut Criterion) { + bench_commit_to_poly::( + c, + &generate_trusted_setup, + ); +} + +fn bench_compute_proof_single_(c: &mut Criterion) { + bench_compute_proof_single::( + c, + &generate_trusted_setup, + ); +} + +criterion_group! { + name = benches; + config = Criterion::default().sample_size(10); + targets = bench_commit_to_poly_, bench_compute_proof_single_ +} + +criterion_main!(benches); diff --git a/constantine/benches/lincomb.rs b/constantine/benches/lincomb.rs new file mode 100644 index 000000000..11118d489 --- /dev/null +++ b/constantine/benches/lincomb.rs @@ -0,0 +1,16 @@ +use criterion::{criterion_group, criterion_main, Criterion}; +use kzg_bench::benches::lincomb::bench_g1_lincomb; +use rust_kzg_zkcrypto::fft_g1::g1_linear_combination; +use rust_kzg_zkcrypto::kzg_types::{ZFr, ZG1}; + +fn bench_g1_lincomb_(c: &mut Criterion) { + bench_g1_lincomb::(c, &g1_linear_combination); +} + +criterion_group! { + name = benches; + config = Criterion::default().sample_size(10); + targets = bench_g1_lincomb_ +} + +criterion_main!(benches); diff --git a/constantine/benches/poly.rs b/constantine/benches/poly.rs new file mode 100644 index 000000000..311f27053 --- /dev/null +++ b/constantine/benches/poly.rs @@ -0,0 +1,16 @@ +use criterion::{criterion_group, criterion_main, Criterion}; +use kzg_bench::benches::poly::bench_new_poly_div; +use rust_kzg_zkcrypto::kzg_types::ZFr; +use rust_kzg_zkcrypto::poly::PolyData; + +fn bench_new_poly_div_(c: &mut Criterion) { + bench_new_poly_div::(c); +} + +criterion_group! { + name = benches; + config = Criterion::default().sample_size(10); + targets = bench_new_poly_div_ +} + +criterion_main!(benches); diff --git a/constantine/benches/recover.rs b/constantine/benches/recover.rs new file mode 100644 index 000000000..0cd28dc75 --- /dev/null +++ b/constantine/benches/recover.rs @@ -0,0 +1,18 @@ +use criterion::{criterion_group, criterion_main, Criterion}; +use kzg_bench::benches::recover::bench_recover; + +use rust_kzg_zkcrypto::kzg_proofs::FFTSettings; +use rust_kzg_zkcrypto::kzg_types::ZFr; +use rust_kzg_zkcrypto::poly::PolyData; + +fn bench_recover_(c: &mut Criterion) { + bench_recover::(c); +} + +criterion_group! { + name = benches; + config = Criterion::default().sample_size(10); + targets = bench_recover_ +} + +criterion_main!(benches); diff --git a/constantine/benches/zero_poly.rs b/constantine/benches/zero_poly.rs new file mode 100644 index 000000000..6fcc9d964 --- /dev/null +++ b/constantine/benches/zero_poly.rs @@ -0,0 +1,18 @@ +use criterion::{criterion_group, criterion_main, Criterion}; +use kzg_bench::benches::zero_poly::bench_zero_poly; + +use rust_kzg_zkcrypto::kzg_proofs::FFTSettings; +use rust_kzg_zkcrypto::kzg_types::ZFr; +use rust_kzg_zkcrypto::poly::PolyData; + +fn bench_zero_poly_(c: &mut Criterion) { + bench_zero_poly::(c); +} + +criterion_group! { + name = benches; + config = Criterion::default().sample_size(10); + targets = bench_zero_poly_ +} + +criterion_main!(benches); diff --git a/constantine/bls12_381/Cargo.toml b/constantine/bls12_381/Cargo.toml new file mode 100644 index 000000000..7d46f8329 --- /dev/null +++ b/constantine/bls12_381/Cargo.toml @@ -0,0 +1,73 @@ +[package] +authors = [ + "Sean Bowe ", + "Jack Grigg ", +] +description = "Implementation of the BLS12-381 pairing-friendly elliptic curve construction" +documentation = "https://docs.rs/bls12_381/" +homepage = "https://github.com/zkcrypto/bls12_381" +license = "MIT/Apache-2.0" +name = "bls12_381" +repository = "https://github.com/zkcrypto/bls12_381" +version = "0.8.0" +edition = "2021" + +[package.metadata.docs.rs] +rustdoc-args = [ "--html-in-header", "katex-header.html" ] + +[dev-dependencies] +csv = ">= 1.0, < 1.2" # csv 1.2 has MSRV 1.60 +criterion = "0.3" +hex = "0.4" +rand_xorshift = "0.3" +sha2 = "0.9" +sha3 = "0.9" + +[[bench]] +name = "groups" +harness = false +required-features = ["groups"] + +[[bench]] +name = "hash_to_curve" +harness = false +required-features = ["experimental"] + +[dependencies.digest] +version = "0.9" +optional = true + +[dependencies.ff] +version = "0.13" +default-features = false + +[dependencies.group] +version = "0.13" +default-features = false +optional = true + +[dependencies.pairing] +version = "0.23" +optional = true + +[dependencies.rand_core] +version = "0.6" +default-features = false + +[dependencies.subtle] +version = "2.2.1" +default-features = false + +[dependencies.zeroize] +version = "1.4" +default-features = false +optional = true + +[features] +default = ["groups", "pairings", "alloc", "bits"] +bits = ["ff/bits"] +groups = ["group"] +pairings = ["groups", "pairing"] +alloc = ["group/alloc"] +experimental = ["digest"] +nightly = ["subtle/nightly"] diff --git a/constantine/bls12_381/benches/groups.rs b/constantine/bls12_381/benches/groups.rs new file mode 100644 index 000000000..87c80d029 --- /dev/null +++ b/constantine/bls12_381/benches/groups.rs @@ -0,0 +1,170 @@ +#[macro_use] +extern crate criterion; + +extern crate bls12_381; +use bls12_381::*; + +use criterion::{black_box, Criterion}; + +fn criterion_benchmark(c: &mut Criterion) { + // Pairings + { + let g = G1Affine::generator(); + let h = G2Affine::generator(); + c.bench_function("full pairing", move |b| { + b.iter(|| pairing(black_box(&g), black_box(&h))) + }); + c.bench_function("G2 preparation for pairing", move |b| { + b.iter(|| G2Prepared::from(h)) + }); + let prep = G2Prepared::from(h); + c.bench_function("miller loop for pairing", move |b| { + b.iter(|| multi_miller_loop(&[(&g, &prep)])) + }); + let prep = G2Prepared::from(h); + let r = multi_miller_loop(&[(&g, &prep)]); + c.bench_function("final exponentiation for pairing", move |b| { + b.iter(|| r.final_exponentiation()) + }); + } + // G1Affine + { + let name = "G1Affine"; + let a = G1Affine::generator(); + let s = Scalar::from_raw([1, 2, 3, 4]); + let compressed = [0u8; 48]; + let uncompressed = [0u8; 96]; + c.bench_function(&format!("{} check on curve", name), move |b| { + b.iter(|| black_box(a).is_on_curve()) + }); + c.bench_function(&format!("{} check equality", name), move |b| { + b.iter(|| black_box(a) == black_box(a)) + }); + c.bench_function(&format!("{} scalar multiplication", name), move |b| { + b.iter(|| black_box(a) * black_box(s)) + }); + c.bench_function(&format!("{} subgroup check", name), move |b| { + b.iter(|| black_box(a).is_torsion_free()) + }); + c.bench_function( + &format!("{} deserialize compressed point", name), + move |b| b.iter(|| G1Affine::from_compressed(black_box(&compressed))), + ); + c.bench_function( + &format!("{} deserialize uncompressed point", name), + move |b| b.iter(|| G1Affine::from_uncompressed(black_box(&uncompressed))), + ); + } + + // G1Projective + { + let name = "G1Projective"; + let a = G1Projective::generator(); + let a_affine = G1Affine::generator(); + let s = Scalar::from_raw([1, 2, 3, 4]); + + const N: usize = 10000; + let v = vec![G1Projective::generator(); N]; + let mut q = vec![G1Affine::identity(); N]; + + c.bench_function(&format!("{} check on curve", name), move |b| { + b.iter(|| black_box(a).is_on_curve()) + }); + c.bench_function(&format!("{} check equality", name), move |b| { + b.iter(|| black_box(a) == black_box(a)) + }); + c.bench_function(&format!("{} to affine", name), move |b| { + b.iter(|| G1Affine::from(black_box(a))) + }); + 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)) + }); + c.bench_function(&format!("{} mixed addition", name), move |b| { + b.iter(|| black_box(a).add_mixed(&a_affine)) + }); + c.bench_function(&format!("{} scalar multiplication", name), move |b| { + b.iter(|| black_box(a) * black_box(s)) + }); + c.bench_function(&format!("{} batch to affine n={}", name, N), move |b| { + b.iter(|| { + G1Projective::batch_normalize(black_box(&v), black_box(&mut q)); + black_box(&q)[0] + }) + }); + } + + // G2Affine + { + let name = "G2Affine"; + let a = G2Affine::generator(); + let s = Scalar::from_raw([1, 2, 3, 4]); + let compressed = [0u8; 96]; + let uncompressed = [0u8; 192]; + c.bench_function(&format!("{} check on curve", name), move |b| { + b.iter(|| black_box(a).is_on_curve()) + }); + c.bench_function(&format!("{} check equality", name), move |b| { + b.iter(|| black_box(a) == black_box(a)) + }); + c.bench_function(&format!("{} scalar multiplication", name), move |b| { + b.iter(|| black_box(a) * black_box(s)) + }); + c.bench_function(&format!("{} subgroup check", name), move |b| { + b.iter(|| black_box(a).is_torsion_free()) + }); + c.bench_function( + &format!("{} deserialize compressed point", name), + move |b| b.iter(|| G2Affine::from_compressed(black_box(&compressed))), + ); + c.bench_function( + &format!("{} deserialize uncompressed point", name), + move |b| b.iter(|| G2Affine::from_uncompressed(black_box(&uncompressed))), + ); + } + + // G2Projective + { + let name = "G2Projective"; + let a = G2Projective::generator(); + let a_affine = G2Affine::generator(); + let s = Scalar::from_raw([1, 2, 3, 4]); + + const N: usize = 10000; + let v = vec![G2Projective::generator(); N]; + let mut q = vec![G2Affine::identity(); N]; + + c.bench_function(&format!("{} check on curve", name), move |b| { + b.iter(|| black_box(a).is_on_curve()) + }); + c.bench_function(&format!("{} check equality", name), move |b| { + b.iter(|| black_box(a) == black_box(a)) + }); + c.bench_function(&format!("{} to affine", name), move |b| { + b.iter(|| G2Affine::from(black_box(a))) + }); + 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)) + }); + c.bench_function(&format!("{} mixed addition", name), move |b| { + b.iter(|| black_box(a).add_mixed(&a_affine)) + }); + c.bench_function(&format!("{} scalar multiplication", name), move |b| { + b.iter(|| black_box(a) * black_box(s)) + }); + c.bench_function(&format!("{} batch to affine n={}", name, N), move |b| { + b.iter(|| { + G2Projective::batch_normalize(black_box(&v), black_box(&mut q)); + black_box(&q)[0] + }) + }); + } +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/constantine/bls12_381/benches/hash_to_curve.rs b/constantine/bls12_381/benches/hash_to_curve.rs new file mode 100644 index 000000000..302b7c6b3 --- /dev/null +++ b/constantine/bls12_381/benches/hash_to_curve.rs @@ -0,0 +1,68 @@ +#[macro_use] +extern crate criterion; + +extern crate bls12_381; +use bls12_381::hash_to_curve::*; +use bls12_381::*; + +use criterion::{black_box, Criterion}; + +fn criterion_benchmark(c: &mut Criterion) { + // G1Projective + { + let name = "G1Projective"; + + let message: &[u8] = b"test message"; + let dst: &[u8] = b"test DST"; + + c.bench_function( + &format!("{} encode_to_curve SSWU SHA-256", name), + move |b| { + b.iter(|| { + >>::encode_to_curve( + black_box(message), + black_box(dst), + ) + }) + }, + ); + c.bench_function(&format!("{} hash_to_curve SSWU SHA-256", name), move |b| { + b.iter(|| { + >>::hash_to_curve( + black_box(message), + black_box(dst), + ) + }) + }); + } + // G2Projective + { + let name = "G2Projective"; + + let message: &[u8] = b"test message"; + let dst: &[u8] = b"test DST"; + + c.bench_function( + &format!("{} encode_to_curve SSWU SHA-256", name), + move |b| { + b.iter(|| { + >>::encode_to_curve( + black_box(message), + black_box(dst), + ) + }) + }, + ); + c.bench_function(&format!("{} hash_to_curve SSWU SHA-256", name), move |b| { + b.iter(|| { + >>::hash_to_curve( + black_box(message), + black_box(dst), + ) + }) + }); + } +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/constantine/bls12_381/src/fp.rs b/constantine/bls12_381/src/fp.rs new file mode 100644 index 000000000..f61150cc6 --- /dev/null +++ b/constantine/bls12_381/src/fp.rs @@ -0,0 +1,990 @@ +//! This module provides an implementation of the BLS12-381 base field `GF(p)` +//! where `p = 0x1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab` +#![allow(clippy::all)] + +use core::fmt; +use core::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}; +use rand_core::RngCore; +use subtle::{Choice, ConditionallySelectable, ConstantTimeEq, CtOption}; + +use crate::util::{adc, mac, sbb}; + +// The internal representation of this type is six 64-bit unsigned +// integers in little-endian order. `Fp` values are always in +// Montgomery form; i.e., Scalar(a) = aR mod p, with R = 2^384. +#[derive(Copy, Clone)] +pub struct Fp(pub [u64; 6]); + +impl fmt::Debug for Fp { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let tmp = self.to_bytes(); + write!(f, "0x")?; + for &b in tmp.iter() { + write!(f, "{:02x}", b)?; + } + Ok(()) + } +} + +impl Default for Fp { + fn default() -> Self { + Fp::zero() + } +} + +#[cfg(feature = "zeroize")] +impl zeroize::DefaultIsZeroes for Fp {} + +impl ConstantTimeEq for Fp { + fn ct_eq(&self, other: &Self) -> Choice { + self.0[0].ct_eq(&other.0[0]) + & self.0[1].ct_eq(&other.0[1]) + & self.0[2].ct_eq(&other.0[2]) + & self.0[3].ct_eq(&other.0[3]) + & self.0[4].ct_eq(&other.0[4]) + & self.0[5].ct_eq(&other.0[5]) + } +} + +impl Eq for Fp {} +impl PartialEq for Fp { + #[inline] + fn eq(&self, other: &Self) -> bool { + bool::from(self.ct_eq(other)) + } +} + +impl ConditionallySelectable for Fp { + fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self { + Fp([ + u64::conditional_select(&a.0[0], &b.0[0], choice), + u64::conditional_select(&a.0[1], &b.0[1], choice), + u64::conditional_select(&a.0[2], &b.0[2], choice), + u64::conditional_select(&a.0[3], &b.0[3], choice), + u64::conditional_select(&a.0[4], &b.0[4], choice), + u64::conditional_select(&a.0[5], &b.0[5], choice), + ]) + } +} + +/// p = 4002409555221667393417789825735904156556882819939007885332058136124031650490837864442687629129015664037894272559787 +const MODULUS: [u64; 6] = [ + 0xb9fe_ffff_ffff_aaab, + 0x1eab_fffe_b153_ffff, + 0x6730_d2a0_f6b0_f624, + 0x6477_4b84_f385_12bf, + 0x4b1b_a7b6_434b_acd7, + 0x1a01_11ea_397f_e69a, +]; + +/// INV = -(p^{-1} mod 2^64) mod 2^64 +const INV: u64 = 0x89f3_fffc_fffc_fffd; + +/// R = 2^384 mod p +const R: Fp = Fp([ + 0x7609_0000_0002_fffd, + 0xebf4_000b_c40c_0002, + 0x5f48_9857_53c7_58ba, + 0x77ce_5853_7052_5745, + 0x5c07_1a97_a256_ec6d, + 0x15f6_5ec3_fa80_e493, +]); + +/// R2 = 2^(384*2) mod p +const R2: Fp = Fp([ + 0xf4df_1f34_1c34_1746, + 0x0a76_e6a6_09d1_04f1, + 0x8de5_476c_4c95_b6d5, + 0x67eb_88a9_939d_83c0, + 0x9a79_3e85_b519_952d, + 0x1198_8fe5_92ca_e3aa, +]); + +/// R3 = 2^(384*3) mod p +const R3: Fp = Fp([ + 0xed48_ac6b_d94c_a1e0, + 0x315f_831e_03a7_adf8, + 0x9a53_352a_615e_29dd, + 0x34c0_4e5e_921e_1761, + 0x2512_d435_6572_4728, + 0x0aa6_3460_9175_5d4d, +]); + +impl<'a> Neg for &'a Fp { + type Output = Fp; + + #[inline] + fn neg(self) -> Fp { + self.neg() + } +} + +impl Neg for Fp { + type Output = Fp; + + #[inline] + fn neg(self) -> Fp { + -&self + } +} + +impl<'a, 'b> Sub<&'b Fp> for &'a Fp { + type Output = Fp; + + #[inline] + fn sub(self, rhs: &'b Fp) -> Fp { + self.sub(rhs) + } +} + +impl<'a, 'b> Add<&'b Fp> for &'a Fp { + type Output = Fp; + + #[inline] + fn add(self, rhs: &'b Fp) -> Fp { + self.add(rhs) + } +} + +impl<'a, 'b> Mul<&'b Fp> for &'a Fp { + type Output = Fp; + + #[inline] + fn mul(self, rhs: &'b Fp) -> Fp { + self.mul(rhs) + } +} + +impl_binops_additive!(Fp, Fp); +impl_binops_multiplicative!(Fp, Fp); + +impl Fp { + /// Returns zero, the additive identity. + #[inline] + pub const fn zero() -> Fp { + Fp([0, 0, 0, 0, 0, 0]) + } + + /// Returns one, the multiplicative identity. + #[inline] + pub const fn one() -> Fp { + R + } + + pub fn is_zero(&self) -> Choice { + self.ct_eq(&Fp::zero()) + } + + /// Attempts to convert a big-endian byte representation of + /// a scalar into an `Fp`, failing if the input is not canonical. + pub fn from_bytes(bytes: &[u8; 48]) -> CtOption { + let mut tmp = Fp([0, 0, 0, 0, 0, 0]); + + tmp.0[5] = u64::from_be_bytes(<[u8; 8]>::try_from(&bytes[0..8]).unwrap()); + tmp.0[4] = u64::from_be_bytes(<[u8; 8]>::try_from(&bytes[8..16]).unwrap()); + tmp.0[3] = u64::from_be_bytes(<[u8; 8]>::try_from(&bytes[16..24]).unwrap()); + tmp.0[2] = u64::from_be_bytes(<[u8; 8]>::try_from(&bytes[24..32]).unwrap()); + tmp.0[1] = u64::from_be_bytes(<[u8; 8]>::try_from(&bytes[32..40]).unwrap()); + tmp.0[0] = u64::from_be_bytes(<[u8; 8]>::try_from(&bytes[40..48]).unwrap()); + + // Try to subtract the modulus + let (_, borrow) = sbb(tmp.0[0], MODULUS[0], 0); + let (_, borrow) = sbb(tmp.0[1], MODULUS[1], borrow); + let (_, borrow) = sbb(tmp.0[2], MODULUS[2], borrow); + let (_, borrow) = sbb(tmp.0[3], MODULUS[3], borrow); + let (_, borrow) = sbb(tmp.0[4], MODULUS[4], borrow); + let (_, borrow) = sbb(tmp.0[5], MODULUS[5], borrow); + + // If the element is smaller than MODULUS then the + // subtraction will underflow, producing a borrow value + // of 0xffff...ffff. Otherwise, it'll be zero. + let is_some = (borrow as u8) & 1; + + // Convert to Montgomery form by computing + // (a.R^0 * R^2) / R = a.R + tmp *= &R2; + + CtOption::new(tmp, Choice::from(is_some)) + } + + /// Converts an element of `Fp` into a byte representation in + /// big-endian byte order. + pub fn to_bytes(self) -> [u8; 48] { + // Turn into canonical form by computing + // (a.R) / R = a + let tmp = Fp::montgomery_reduce( + self.0[0], self.0[1], self.0[2], self.0[3], self.0[4], self.0[5], 0, 0, 0, 0, 0, 0, + ); + + let mut res = [0; 48]; + res[0..8].copy_from_slice(&tmp.0[5].to_be_bytes()); + res[8..16].copy_from_slice(&tmp.0[4].to_be_bytes()); + res[16..24].copy_from_slice(&tmp.0[3].to_be_bytes()); + res[24..32].copy_from_slice(&tmp.0[2].to_be_bytes()); + res[32..40].copy_from_slice(&tmp.0[1].to_be_bytes()); + res[40..48].copy_from_slice(&tmp.0[0].to_be_bytes()); + + res + } + + pub(crate) fn random(mut rng: impl RngCore) -> Fp { + let mut bytes = [0u8; 96]; + rng.fill_bytes(&mut bytes); + + // Parse the random bytes as a big-endian number, to match Fp encoding order. + Fp::from_u768([ + u64::from_be_bytes(<[u8; 8]>::try_from(&bytes[0..8]).unwrap()), + u64::from_be_bytes(<[u8; 8]>::try_from(&bytes[8..16]).unwrap()), + u64::from_be_bytes(<[u8; 8]>::try_from(&bytes[16..24]).unwrap()), + u64::from_be_bytes(<[u8; 8]>::try_from(&bytes[24..32]).unwrap()), + u64::from_be_bytes(<[u8; 8]>::try_from(&bytes[32..40]).unwrap()), + u64::from_be_bytes(<[u8; 8]>::try_from(&bytes[40..48]).unwrap()), + u64::from_be_bytes(<[u8; 8]>::try_from(&bytes[48..56]).unwrap()), + u64::from_be_bytes(<[u8; 8]>::try_from(&bytes[56..64]).unwrap()), + u64::from_be_bytes(<[u8; 8]>::try_from(&bytes[64..72]).unwrap()), + u64::from_be_bytes(<[u8; 8]>::try_from(&bytes[72..80]).unwrap()), + u64::from_be_bytes(<[u8; 8]>::try_from(&bytes[80..88]).unwrap()), + u64::from_be_bytes(<[u8; 8]>::try_from(&bytes[88..96]).unwrap()), + ]) + } + + /// Reduces a big-endian 64-bit limb representation of a 768-bit number. + fn from_u768(limbs: [u64; 12]) -> Fp { + // We reduce an arbitrary 768-bit number by decomposing it into two 384-bit digits + // with the higher bits multiplied by 2^384. Thus, we perform two reductions + // + // 1. the lower bits are multiplied by R^2, as normal + // 2. the upper bits are multiplied by R^2 * 2^384 = R^3 + // + // and computing their sum in the field. It remains to see that arbitrary 384-bit + // numbers can be placed into Montgomery form safely using the reduction. The + // reduction works so long as the product is less than R=2^384 multiplied by + // the modulus. This holds because for any `c` smaller than the modulus, we have + // that (2^384 - 1)*c is an acceptable product for the reduction. Therefore, the + // reduction always works so long as `c` is in the field; in this case it is either the + // constant `R2` or `R3`. + let d1 = Fp([limbs[11], limbs[10], limbs[9], limbs[8], limbs[7], limbs[6]]); + let d0 = Fp([limbs[5], limbs[4], limbs[3], limbs[2], limbs[1], limbs[0]]); + // Convert to Montgomery form + d0 * R2 + d1 * R3 + } + + /// Returns whether or not this element is strictly lexicographically + /// larger than its negation. + pub fn lexicographically_largest(&self) -> Choice { + // This can be determined by checking to see if the element is + // larger than (p - 1) // 2. If we subtract by ((p - 1) // 2) + 1 + // and there is no underflow, then the element must be larger than + // (p - 1) // 2. + + // First, because self is in Montgomery form we need to reduce it + let tmp = Fp::montgomery_reduce( + self.0[0], self.0[1], self.0[2], self.0[3], self.0[4], self.0[5], 0, 0, 0, 0, 0, 0, + ); + + let (_, borrow) = sbb(tmp.0[0], 0xdcff_7fff_ffff_d556, 0); + let (_, borrow) = sbb(tmp.0[1], 0x0f55_ffff_58a9_ffff, borrow); + let (_, borrow) = sbb(tmp.0[2], 0xb398_6950_7b58_7b12, borrow); + let (_, borrow) = sbb(tmp.0[3], 0xb23b_a5c2_79c2_895f, borrow); + let (_, borrow) = sbb(tmp.0[4], 0x258d_d3db_21a5_d66b, borrow); + let (_, borrow) = sbb(tmp.0[5], 0x0d00_88f5_1cbf_f34d, borrow); + + // If the element was smaller, the subtraction will underflow + // producing a borrow value of 0xffff...ffff, otherwise it will + // be zero. We create a Choice representing true if there was + // overflow (and so this element is not lexicographically larger + // than its negation) and then negate it. + + !Choice::from((borrow as u8) & 1) + } + + /// Constructs an element of `Fp` without checking that it is + /// canonical. + pub const fn from_raw_unchecked(v: [u64; 6]) -> Fp { + Fp(v) + } + + /// Although this is labeled "vartime", it is only + /// variable time with respect to the exponent. It + /// is also not exposed in the public API. + pub fn pow_vartime(&self, by: &[u64; 6]) -> Self { + let mut res = Self::one(); + for e in by.iter().rev() { + for i in (0..64).rev() { + res = res.square(); + + if ((*e >> i) & 1) == 1 { + res *= self; + } + } + } + res + } + + #[inline] + pub fn sqrt(&self) -> CtOption { + // We use Shank's method, as p = 3 (mod 4). This means + // we only need to exponentiate by (p+1)/4. This only + // works for elements that are actually quadratic residue, + // so we check that we got the correct result at the end. + + let sqrt = self.pow_vartime(&[ + 0xee7f_bfff_ffff_eaab, + 0x07aa_ffff_ac54_ffff, + 0xd9cc_34a8_3dac_3d89, + 0xd91d_d2e1_3ce1_44af, + 0x92c6_e9ed_90d2_eb35, + 0x0680_447a_8e5f_f9a6, + ]); + + CtOption::new(sqrt, sqrt.square().ct_eq(self)) + } + + #[inline] + /// Computes the multiplicative inverse of this field + /// element, returning None in the case that this element + /// is zero. + pub fn invert(&self) -> CtOption { + // Exponentiate by p - 2 + let t = self.pow_vartime(&[ + 0xb9fe_ffff_ffff_aaa9, + 0x1eab_fffe_b153_ffff, + 0x6730_d2a0_f6b0_f624, + 0x6477_4b84_f385_12bf, + 0x4b1b_a7b6_434b_acd7, + 0x1a01_11ea_397f_e69a, + ]); + + CtOption::new(t, !self.is_zero()) + } + + #[inline] + const fn subtract_p(&self) -> Fp { + let (r0, borrow) = sbb(self.0[0], MODULUS[0], 0); + let (r1, borrow) = sbb(self.0[1], MODULUS[1], borrow); + let (r2, borrow) = sbb(self.0[2], MODULUS[2], borrow); + let (r3, borrow) = sbb(self.0[3], MODULUS[3], borrow); + let (r4, borrow) = sbb(self.0[4], MODULUS[4], borrow); + let (r5, borrow) = sbb(self.0[5], MODULUS[5], borrow); + + // If underflow occurred on the final limb, borrow = 0xfff...fff, otherwise + // borrow = 0x000...000. Thus, we use it as a mask! + let r0 = (self.0[0] & borrow) | (r0 & !borrow); + let r1 = (self.0[1] & borrow) | (r1 & !borrow); + let r2 = (self.0[2] & borrow) | (r2 & !borrow); + let r3 = (self.0[3] & borrow) | (r3 & !borrow); + let r4 = (self.0[4] & borrow) | (r4 & !borrow); + let r5 = (self.0[5] & borrow) | (r5 & !borrow); + + Fp([r0, r1, r2, r3, r4, r5]) + } + + #[inline] + pub const fn add(&self, rhs: &Fp) -> Fp { + let (d0, carry) = adc(self.0[0], rhs.0[0], 0); + let (d1, carry) = adc(self.0[1], rhs.0[1], carry); + let (d2, carry) = adc(self.0[2], rhs.0[2], carry); + let (d3, carry) = adc(self.0[3], rhs.0[3], carry); + let (d4, carry) = adc(self.0[4], rhs.0[4], carry); + let (d5, _) = adc(self.0[5], rhs.0[5], carry); + + // Attempt to subtract the modulus, to ensure the value + // is smaller than the modulus. + (&Fp([d0, d1, d2, d3, d4, d5])).subtract_p() + } + + #[inline] + pub const fn neg(&self) -> Fp { + let (d0, borrow) = sbb(MODULUS[0], self.0[0], 0); + let (d1, borrow) = sbb(MODULUS[1], self.0[1], borrow); + let (d2, borrow) = sbb(MODULUS[2], self.0[2], borrow); + let (d3, borrow) = sbb(MODULUS[3], self.0[3], borrow); + let (d4, borrow) = sbb(MODULUS[4], self.0[4], borrow); + let (d5, _) = sbb(MODULUS[5], self.0[5], borrow); + + // Let's use a mask if `self` was zero, which would mean + // the result of the subtraction is p. + let mask = (((self.0[0] | self.0[1] | self.0[2] | self.0[3] | self.0[4] | self.0[5]) == 0) + as u64) + .wrapping_sub(1); + + Fp([ + d0 & mask, + d1 & mask, + d2 & mask, + d3 & mask, + d4 & mask, + d5 & mask, + ]) + } + + #[inline] + pub const fn sub(&self, rhs: &Fp) -> Fp { + (&rhs.neg()).add(self) + } + + /// Returns `c = a.zip(b).fold(0, |acc, (a_i, b_i)| acc + a_i * b_i)`. + /// + /// Implements Algorithm 2 from Patrick Longa's + /// [ePrint 2022-367](https://eprint.iacr.org/2022/367) ยง3. + #[inline] + pub(crate) fn sum_of_products(a: [Fp; T], b: [Fp; T]) -> Fp { + // For a single `a x b` multiplication, operand scanning (schoolbook) takes each + // limb of `a` in turn, and multiplies it by all of the limbs of `b` to compute + // the result as a double-width intermediate representation, which is then fully + // reduced at the end. Here however we have pairs of multiplications (a_i, b_i), + // the results of which are summed. + // + // The intuition for this algorithm is two-fold: + // - We can interleave the operand scanning for each pair, by processing the jth + // limb of each `a_i` together. As these have the same offset within the overall + // operand scanning flow, their results can be summed directly. + // - We can interleave the multiplication and reduction steps, resulting in a + // single bitshift by the limb size after each iteration. This means we only + // need to store a single extra limb overall, instead of keeping around all the + // intermediate results and eventually having twice as many limbs. + + // Algorithm 2, line 2 + let (u0, u1, u2, u3, u4, u5) = + (0..6).fold((0, 0, 0, 0, 0, 0), |(u0, u1, u2, u3, u4, u5), j| { + // Algorithm 2, line 3 + // For each pair in the overall sum of products: + let (t0, t1, t2, t3, t4, t5, t6) = (0..T).fold( + (u0, u1, u2, u3, u4, u5, 0), + |(t0, t1, t2, t3, t4, t5, t6), i| { + // Compute digit_j x row and accumulate into `u`. + let (t0, carry) = mac(t0, a[i].0[j], b[i].0[0], 0); + let (t1, carry) = mac(t1, a[i].0[j], b[i].0[1], carry); + let (t2, carry) = mac(t2, a[i].0[j], b[i].0[2], carry); + let (t3, carry) = mac(t3, a[i].0[j], b[i].0[3], carry); + let (t4, carry) = mac(t4, a[i].0[j], b[i].0[4], carry); + let (t5, carry) = mac(t5, a[i].0[j], b[i].0[5], carry); + let (t6, _) = adc(t6, 0, carry); + + (t0, t1, t2, t3, t4, t5, t6) + }, + ); + + // Algorithm 2, lines 4-5 + // This is a single step of the usual Montgomery reduction process. + let k = t0.wrapping_mul(INV); + let (_, carry) = mac(t0, k, MODULUS[0], 0); + let (r1, carry) = mac(t1, k, MODULUS[1], carry); + let (r2, carry) = mac(t2, k, MODULUS[2], carry); + let (r3, carry) = mac(t3, k, MODULUS[3], carry); + let (r4, carry) = mac(t4, k, MODULUS[4], carry); + let (r5, carry) = mac(t5, k, MODULUS[5], carry); + let (r6, _) = adc(t6, 0, carry); + + (r1, r2, r3, r4, r5, r6) + }); + + // Because we represent F_p elements in non-redundant form, we need a final + // conditional subtraction to ensure the output is in range. + (&Fp([u0, u1, u2, u3, u4, u5])).subtract_p() + } + + #[inline(always)] + pub(crate) const fn montgomery_reduce( + t0: u64, + t1: u64, + t2: u64, + t3: u64, + t4: u64, + t5: u64, + t6: u64, + t7: u64, + t8: u64, + t9: u64, + t10: u64, + t11: u64, + ) -> Self { + // The Montgomery reduction here is based on Algorithm 14.32 in + // Handbook of Applied Cryptography + // . + + let k = t0.wrapping_mul(INV); + let (_, carry) = mac(t0, k, MODULUS[0], 0); + let (r1, carry) = mac(t1, k, MODULUS[1], carry); + let (r2, carry) = mac(t2, k, MODULUS[2], carry); + let (r3, carry) = mac(t3, k, MODULUS[3], carry); + let (r4, carry) = mac(t4, k, MODULUS[4], carry); + let (r5, carry) = mac(t5, k, MODULUS[5], carry); + let (r6, r7) = adc(t6, 0, carry); + + let k = r1.wrapping_mul(INV); + let (_, carry) = mac(r1, k, MODULUS[0], 0); + let (r2, carry) = mac(r2, k, MODULUS[1], carry); + let (r3, carry) = mac(r3, k, MODULUS[2], carry); + let (r4, carry) = mac(r4, k, MODULUS[3], carry); + let (r5, carry) = mac(r5, k, MODULUS[4], carry); + let (r6, carry) = mac(r6, k, MODULUS[5], carry); + let (r7, r8) = adc(t7, r7, carry); + + let k = r2.wrapping_mul(INV); + let (_, carry) = mac(r2, k, MODULUS[0], 0); + let (r3, carry) = mac(r3, k, MODULUS[1], carry); + let (r4, carry) = mac(r4, k, MODULUS[2], carry); + let (r5, carry) = mac(r5, k, MODULUS[3], carry); + let (r6, carry) = mac(r6, k, MODULUS[4], carry); + let (r7, carry) = mac(r7, k, MODULUS[5], carry); + let (r8, r9) = adc(t8, r8, carry); + + let k = r3.wrapping_mul(INV); + let (_, carry) = mac(r3, k, MODULUS[0], 0); + let (r4, carry) = mac(r4, k, MODULUS[1], carry); + let (r5, carry) = mac(r5, k, MODULUS[2], carry); + let (r6, carry) = mac(r6, k, MODULUS[3], carry); + let (r7, carry) = mac(r7, k, MODULUS[4], carry); + let (r8, carry) = mac(r8, k, MODULUS[5], carry); + let (r9, r10) = adc(t9, r9, carry); + + let k = r4.wrapping_mul(INV); + let (_, carry) = mac(r4, k, MODULUS[0], 0); + let (r5, carry) = mac(r5, k, MODULUS[1], carry); + let (r6, carry) = mac(r6, k, MODULUS[2], carry); + let (r7, carry) = mac(r7, k, MODULUS[3], carry); + let (r8, carry) = mac(r8, k, MODULUS[4], carry); + let (r9, carry) = mac(r9, k, MODULUS[5], carry); + let (r10, r11) = adc(t10, r10, carry); + + let k = r5.wrapping_mul(INV); + let (_, carry) = mac(r5, k, MODULUS[0], 0); + let (r6, carry) = mac(r6, k, MODULUS[1], carry); + let (r7, carry) = mac(r7, k, MODULUS[2], carry); + let (r8, carry) = mac(r8, k, MODULUS[3], carry); + let (r9, carry) = mac(r9, k, MODULUS[4], carry); + let (r10, carry) = mac(r10, k, MODULUS[5], carry); + let (r11, _) = adc(t11, r11, carry); + + // Attempt to subtract the modulus, to ensure the value + // is smaller than the modulus. + (&Fp([r6, r7, r8, r9, r10, r11])).subtract_p() + } + + #[inline] + pub const fn mul(&self, rhs: &Fp) -> Fp { + let (t0, carry) = mac(0, self.0[0], rhs.0[0], 0); + let (t1, carry) = mac(0, self.0[0], rhs.0[1], carry); + let (t2, carry) = mac(0, self.0[0], rhs.0[2], carry); + let (t3, carry) = mac(0, self.0[0], rhs.0[3], carry); + let (t4, carry) = mac(0, self.0[0], rhs.0[4], carry); + let (t5, t6) = mac(0, self.0[0], rhs.0[5], carry); + + let (t1, carry) = mac(t1, self.0[1], rhs.0[0], 0); + let (t2, carry) = mac(t2, self.0[1], rhs.0[1], carry); + let (t3, carry) = mac(t3, self.0[1], rhs.0[2], carry); + let (t4, carry) = mac(t4, self.0[1], rhs.0[3], carry); + let (t5, carry) = mac(t5, self.0[1], rhs.0[4], carry); + let (t6, t7) = mac(t6, self.0[1], rhs.0[5], carry); + + let (t2, carry) = mac(t2, self.0[2], rhs.0[0], 0); + let (t3, carry) = mac(t3, self.0[2], rhs.0[1], carry); + let (t4, carry) = mac(t4, self.0[2], rhs.0[2], carry); + let (t5, carry) = mac(t5, self.0[2], rhs.0[3], carry); + let (t6, carry) = mac(t6, self.0[2], rhs.0[4], carry); + let (t7, t8) = mac(t7, self.0[2], rhs.0[5], carry); + + let (t3, carry) = mac(t3, self.0[3], rhs.0[0], 0); + let (t4, carry) = mac(t4, self.0[3], rhs.0[1], carry); + let (t5, carry) = mac(t5, self.0[3], rhs.0[2], carry); + let (t6, carry) = mac(t6, self.0[3], rhs.0[3], carry); + let (t7, carry) = mac(t7, self.0[3], rhs.0[4], carry); + let (t8, t9) = mac(t8, self.0[3], rhs.0[5], carry); + + let (t4, carry) = mac(t4, self.0[4], rhs.0[0], 0); + let (t5, carry) = mac(t5, self.0[4], rhs.0[1], carry); + let (t6, carry) = mac(t6, self.0[4], rhs.0[2], carry); + let (t7, carry) = mac(t7, self.0[4], rhs.0[3], carry); + let (t8, carry) = mac(t8, self.0[4], rhs.0[4], carry); + let (t9, t10) = mac(t9, self.0[4], rhs.0[5], carry); + + let (t5, carry) = mac(t5, self.0[5], rhs.0[0], 0); + let (t6, carry) = mac(t6, self.0[5], rhs.0[1], carry); + let (t7, carry) = mac(t7, self.0[5], rhs.0[2], carry); + let (t8, carry) = mac(t8, self.0[5], rhs.0[3], carry); + let (t9, carry) = mac(t9, self.0[5], rhs.0[4], carry); + let (t10, t11) = mac(t10, self.0[5], rhs.0[5], carry); + + Self::montgomery_reduce(t0, t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11) + } + + /// Squares this element. + #[inline] + pub const fn square(&self) -> Self { + let (t1, carry) = mac(0, self.0[0], self.0[1], 0); + let (t2, carry) = mac(0, self.0[0], self.0[2], carry); + let (t3, carry) = mac(0, self.0[0], self.0[3], carry); + let (t4, carry) = mac(0, self.0[0], self.0[4], carry); + let (t5, t6) = mac(0, self.0[0], self.0[5], carry); + + let (t3, carry) = mac(t3, self.0[1], self.0[2], 0); + let (t4, carry) = mac(t4, self.0[1], self.0[3], carry); + let (t5, carry) = mac(t5, self.0[1], self.0[4], carry); + let (t6, t7) = mac(t6, self.0[1], self.0[5], carry); + + let (t5, carry) = mac(t5, self.0[2], self.0[3], 0); + let (t6, carry) = mac(t6, self.0[2], self.0[4], carry); + let (t7, t8) = mac(t7, self.0[2], self.0[5], carry); + + let (t7, carry) = mac(t7, self.0[3], self.0[4], 0); + let (t8, t9) = mac(t8, self.0[3], self.0[5], carry); + + let (t9, t10) = mac(t9, self.0[4], self.0[5], 0); + + let t11 = t10 >> 63; + let t10 = (t10 << 1) | (t9 >> 63); + let t9 = (t9 << 1) | (t8 >> 63); + let t8 = (t8 << 1) | (t7 >> 63); + let t7 = (t7 << 1) | (t6 >> 63); + let t6 = (t6 << 1) | (t5 >> 63); + let t5 = (t5 << 1) | (t4 >> 63); + let t4 = (t4 << 1) | (t3 >> 63); + let t3 = (t3 << 1) | (t2 >> 63); + let t2 = (t2 << 1) | (t1 >> 63); + let t1 = t1 << 1; + + let (t0, carry) = mac(0, self.0[0], self.0[0], 0); + let (t1, carry) = adc(t1, 0, carry); + let (t2, carry) = mac(t2, self.0[1], self.0[1], carry); + let (t3, carry) = adc(t3, 0, carry); + let (t4, carry) = mac(t4, self.0[2], self.0[2], carry); + let (t5, carry) = adc(t5, 0, carry); + let (t6, carry) = mac(t6, self.0[3], self.0[3], carry); + let (t7, carry) = adc(t7, 0, carry); + let (t8, carry) = mac(t8, self.0[4], self.0[4], carry); + let (t9, carry) = adc(t9, 0, carry); + let (t10, carry) = mac(t10, self.0[5], self.0[5], carry); + let (t11, _) = adc(t11, 0, carry); + + Self::montgomery_reduce(t0, t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11) + } +} + +#[test] +fn test_conditional_selection() { + let a = Fp([1, 2, 3, 4, 5, 6]); + let b = Fp([7, 8, 9, 10, 11, 12]); + + assert_eq!( + ConditionallySelectable::conditional_select(&a, &b, Choice::from(0u8)), + a + ); + assert_eq!( + ConditionallySelectable::conditional_select(&a, &b, Choice::from(1u8)), + b + ); +} + +#[test] +fn test_equality() { + fn is_equal(a: &Fp, b: &Fp) -> bool { + let eq = a == b; + let ct_eq = a.ct_eq(&b); + + assert_eq!(eq, bool::from(ct_eq)); + + eq + } + + assert!(is_equal(&Fp([1, 2, 3, 4, 5, 6]), &Fp([1, 2, 3, 4, 5, 6]))); + + assert!(!is_equal(&Fp([7, 2, 3, 4, 5, 6]), &Fp([1, 2, 3, 4, 5, 6]))); + assert!(!is_equal(&Fp([1, 7, 3, 4, 5, 6]), &Fp([1, 2, 3, 4, 5, 6]))); + assert!(!is_equal(&Fp([1, 2, 7, 4, 5, 6]), &Fp([1, 2, 3, 4, 5, 6]))); + assert!(!is_equal(&Fp([1, 2, 3, 7, 5, 6]), &Fp([1, 2, 3, 4, 5, 6]))); + assert!(!is_equal(&Fp([1, 2, 3, 4, 7, 6]), &Fp([1, 2, 3, 4, 5, 6]))); + assert!(!is_equal(&Fp([1, 2, 3, 4, 5, 7]), &Fp([1, 2, 3, 4, 5, 6]))); +} + +#[test] +fn test_squaring() { + let a = Fp([ + 0xd215_d276_8e83_191b, + 0x5085_d80f_8fb2_8261, + 0xce9a_032d_df39_3a56, + 0x3e9c_4fff_2ca0_c4bb, + 0x6436_b6f7_f4d9_5dfb, + 0x1060_6628_ad4a_4d90, + ]); + let b = Fp([ + 0x33d9_c42a_3cb3_e235, + 0xdad1_1a09_4c4c_d455, + 0xa2f1_44bd_729a_aeba, + 0xd415_0932_be9f_feac, + 0xe27b_c7c4_7d44_ee50, + 0x14b6_a78d_3ec7_a560, + ]); + + assert_eq!(a.square(), b); +} + +#[test] +fn test_multiplication() { + let a = Fp([ + 0x0397_a383_2017_0cd4, + 0x734c_1b2c_9e76_1d30, + 0x5ed2_55ad_9a48_beb5, + 0x095a_3c6b_22a7_fcfc, + 0x2294_ce75_d4e2_6a27, + 0x1333_8bd8_7001_1ebb, + ]); + let b = Fp([ + 0xb9c3_c7c5_b119_6af7, + 0x2580_e208_6ce3_35c1, + 0xf49a_ed3d_8a57_ef42, + 0x41f2_81e4_9846_e878, + 0xe076_2346_c384_52ce, + 0x0652_e893_26e5_7dc0, + ]); + let c = Fp([ + 0xf96e_f3d7_11ab_5355, + 0xe8d4_59ea_00f1_48dd, + 0x53f7_354a_5f00_fa78, + 0x9e34_a4f3_125c_5f83, + 0x3fbe_0c47_ca74_c19e, + 0x01b0_6a8b_bd4a_dfe4, + ]); + + assert_eq!(a * b, c); +} + +#[test] +fn test_addition() { + let a = Fp([ + 0x5360_bb59_7867_8032, + 0x7dd2_75ae_799e_128e, + 0x5c5b_5071_ce4f_4dcf, + 0xcdb2_1f93_078d_bb3e, + 0xc323_65c5_e73f_474a, + 0x115a_2a54_89ba_be5b, + ]); + let b = Fp([ + 0x9fd2_8773_3d23_dda0, + 0xb16b_f2af_738b_3554, + 0x3e57_a75b_d3cc_6d1d, + 0x900b_c0bd_627f_d6d6, + 0xd319_a080_efb2_45fe, + 0x15fd_caa4_e4bb_2091, + ]); + let c = Fp([ + 0x3934_42cc_b58b_b327, + 0x1092_685f_3bd5_47e3, + 0x3382_252c_ab6a_c4c9, + 0xf946_94cb_7688_7f55, + 0x4b21_5e90_93a5_e071, + 0x0d56_e30f_34f5_f853, + ]); + + assert_eq!(a + b, c); +} + +#[test] +fn test_subtraction() { + let a = Fp([ + 0x5360_bb59_7867_8032, + 0x7dd2_75ae_799e_128e, + 0x5c5b_5071_ce4f_4dcf, + 0xcdb2_1f93_078d_bb3e, + 0xc323_65c5_e73f_474a, + 0x115a_2a54_89ba_be5b, + ]); + let b = Fp([ + 0x9fd2_8773_3d23_dda0, + 0xb16b_f2af_738b_3554, + 0x3e57_a75b_d3cc_6d1d, + 0x900b_c0bd_627f_d6d6, + 0xd319_a080_efb2_45fe, + 0x15fd_caa4_e4bb_2091, + ]); + let c = Fp([ + 0x6d8d_33e6_3b43_4d3d, + 0xeb12_82fd_b766_dd39, + 0x8534_7bb6_f133_d6d5, + 0xa21d_aa5a_9892_f727, + 0x3b25_6cfb_3ad8_ae23, + 0x155d_7199_de7f_8464, + ]); + + assert_eq!(a - b, c); +} + +#[test] +fn test_negation() { + let a = Fp([ + 0x5360_bb59_7867_8032, + 0x7dd2_75ae_799e_128e, + 0x5c5b_5071_ce4f_4dcf, + 0xcdb2_1f93_078d_bb3e, + 0xc323_65c5_e73f_474a, + 0x115a_2a54_89ba_be5b, + ]); + let b = Fp([ + 0x669e_44a6_8798_2a79, + 0xa0d9_8a50_37b5_ed71, + 0x0ad5_822f_2861_a854, + 0x96c5_2bf1_ebf7_5781, + 0x87f8_41f0_5c0c_658c, + 0x08a6_e795_afc5_283e, + ]); + + assert_eq!(-a, b); +} + +#[test] +fn test_debug() { + assert_eq!( + format!( + "{:?}", + Fp([ + 0x5360_bb59_7867_8032, + 0x7dd2_75ae_799e_128e, + 0x5c5b_5071_ce4f_4dcf, + 0xcdb2_1f93_078d_bb3e, + 0xc323_65c5_e73f_474a, + 0x115a_2a54_89ba_be5b, + ]) + ), + "0x104bf052ad3bc99bcb176c24a06a6c3aad4eaf2308fc4d282e106c84a757d061052630515305e59bdddf8111bfdeb704" + ); +} + +#[test] +fn test_from_bytes() { + let mut a = Fp([ + 0xdc90_6d9b_e3f9_5dc8, + 0x8755_caf7_4596_91a1, + 0xcff1_a7f4_e958_3ab3, + 0x9b43_821f_849e_2284, + 0xf575_54f3_a297_4f3f, + 0x085d_bea8_4ed4_7f79, + ]); + + for _ in 0..100 { + a = a.square(); + let tmp = a.to_bytes(); + let b = Fp::from_bytes(&tmp).unwrap(); + + assert_eq!(a, b); + } + + assert_eq!( + -Fp::one(), + Fp::from_bytes(&[ + 26, 1, 17, 234, 57, 127, 230, 154, 75, 27, 167, 182, 67, 75, 172, 215, 100, 119, 75, + 132, 243, 133, 18, 191, 103, 48, 210, 160, 246, 176, 246, 36, 30, 171, 255, 254, 177, + 83, 255, 255, 185, 254, 255, 255, 255, 255, 170, 170 + ]) + .unwrap() + ); + + assert!(bool::from( + Fp::from_bytes(&[ + 27, 1, 17, 234, 57, 127, 230, 154, 75, 27, 167, 182, 67, 75, 172, 215, 100, 119, 75, + 132, 243, 133, 18, 191, 103, 48, 210, 160, 246, 176, 246, 36, 30, 171, 255, 254, 177, + 83, 255, 255, 185, 254, 255, 255, 255, 255, 170, 170 + ]) + .is_none() + )); + + assert!(bool::from(Fp::from_bytes(&[0xff; 48]).is_none())); +} + +#[test] +fn test_sqrt() { + // a = 4 + let a = Fp::from_raw_unchecked([ + 0xaa27_0000_000c_fff3, + 0x53cc_0032_fc34_000a, + 0x478f_e97a_6b0a_807f, + 0xb1d3_7ebe_e6ba_24d7, + 0x8ec9_733b_bf78_ab2f, + 0x09d6_4551_3d83_de7e, + ]); + + assert_eq!( + // sqrt(4) = -2 + -a.sqrt().unwrap(), + // 2 + Fp::from_raw_unchecked([ + 0x3213_0000_0006_554f, + 0xb93c_0018_d6c4_0005, + 0x5760_5e0d_b0dd_bb51, + 0x8b25_6521_ed1f_9bcb, + 0x6cf2_8d79_0162_2c03, + 0x11eb_ab9d_bb81_e28c, + ]) + ); +} + +#[test] +fn test_inversion() { + let a = Fp([ + 0x43b4_3a50_78ac_2076, + 0x1ce0_7630_46f8_962b, + 0x724a_5276_486d_735c, + 0x6f05_c2a6_282d_48fd, + 0x2095_bd5b_b4ca_9331, + 0x03b3_5b38_94b0_f7da, + ]); + let b = Fp([ + 0x69ec_d704_0952_148f, + 0x985c_cc20_2219_0f55, + 0xe19b_ba36_a9ad_2f41, + 0x19bb_16c9_5219_dbd8, + 0x14dc_acfd_fb47_8693, + 0x115f_f58a_fff9_a8e1, + ]); + + assert_eq!(a.invert().unwrap(), b); + assert!(bool::from(Fp::zero().invert().is_none())); +} + +#[test] +fn test_lexicographic_largest() { + assert!(!bool::from(Fp::zero().lexicographically_largest())); + assert!(!bool::from(Fp::one().lexicographically_largest())); + assert!(!bool::from( + Fp::from_raw_unchecked([ + 0xa1fa_ffff_fffe_5557, + 0x995b_fff9_76a3_fffe, + 0x03f4_1d24_d174_ceb4, + 0xf654_7998_c199_5dbd, + 0x778a_468f_507a_6034, + 0x0205_5993_1f7f_8103 + ]) + .lexicographically_largest() + )); + assert!(bool::from( + Fp::from_raw_unchecked([ + 0x1804_0000_0001_5554, + 0x8550_0005_3ab0_0001, + 0x633c_b57c_253c_276f, + 0x6e22_d1ec_31eb_b502, + 0xd391_6126_f2d1_4ca2, + 0x17fb_b857_1a00_6596, + ]) + .lexicographically_largest() + )); + assert!(bool::from( + Fp::from_raw_unchecked([ + 0x43f5_ffff_fffc_aaae, + 0x32b7_fff2_ed47_fffd, + 0x07e8_3a49_a2e9_9d69, + 0xeca8_f331_8332_bb7a, + 0xef14_8d1e_a0f4_c069, + 0x040a_b326_3eff_0206, + ]) + .lexicographically_largest() + )); +} + +#[cfg(feature = "zeroize")] +#[test] +fn test_zeroize() { + use zeroize::Zeroize; + + let mut a = Fp::one(); + a.zeroize(); + assert!(bool::from(a.is_zero())); +} diff --git a/constantine/bls12_381/src/fp12.rs b/constantine/bls12_381/src/fp12.rs new file mode 100644 index 000000000..26b48043a --- /dev/null +++ b/constantine/bls12_381/src/fp12.rs @@ -0,0 +1,659 @@ +use crate::fp::*; +use crate::fp2::*; +use crate::fp6::*; + +use core::fmt; +use core::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}; +use subtle::{Choice, ConditionallySelectable, ConstantTimeEq, CtOption}; + +#[cfg(feature = "pairings")] +use rand_core::RngCore; + +/// This represents an element $c_0 + c_1 w$ of $\mathbb{F}_{p^12} = \mathbb{F}_{p^6} / w^2 - v$. +pub struct Fp12 { + pub c0: Fp6, + pub c1: Fp6, +} + +impl From for Fp12 { + fn from(f: Fp) -> Fp12 { + Fp12 { + c0: Fp6::from(f), + c1: Fp6::zero(), + } + } +} + +impl From for Fp12 { + fn from(f: Fp2) -> Fp12 { + Fp12 { + c0: Fp6::from(f), + c1: Fp6::zero(), + } + } +} + +impl From for Fp12 { + fn from(f: Fp6) -> Fp12 { + Fp12 { + c0: f, + c1: Fp6::zero(), + } + } +} + +impl PartialEq for Fp12 { + fn eq(&self, other: &Fp12) -> bool { + self.ct_eq(other).into() + } +} + +impl Copy for Fp12 {} +impl Clone for Fp12 { + #[inline] + fn clone(&self) -> Self { + *self + } +} + +impl Default for Fp12 { + fn default() -> Self { + Fp12::zero() + } +} + +#[cfg(feature = "zeroize")] +impl zeroize::DefaultIsZeroes for Fp12 {} + +impl fmt::Debug for Fp12 { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?} + ({:?})*w", self.c0, self.c1) + } +} + +impl ConditionallySelectable for Fp12 { + #[inline(always)] + fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self { + Fp12 { + c0: Fp6::conditional_select(&a.c0, &b.c0, choice), + c1: Fp6::conditional_select(&a.c1, &b.c1, choice), + } + } +} + +impl ConstantTimeEq for Fp12 { + #[inline(always)] + fn ct_eq(&self, other: &Self) -> Choice { + self.c0.ct_eq(&other.c0) & self.c1.ct_eq(&other.c1) + } +} + +impl Fp12 { + #[inline] + pub fn zero() -> Self { + Fp12 { + c0: Fp6::zero(), + c1: Fp6::zero(), + } + } + + #[inline] + pub fn one() -> Self { + Fp12 { + c0: Fp6::one(), + c1: Fp6::zero(), + } + } + + #[cfg(feature = "pairings")] + pub(crate) fn random(mut rng: impl RngCore) -> Self { + Fp12 { + c0: Fp6::random(&mut rng), + c1: Fp6::random(&mut rng), + } + } + + pub fn mul_by_014(&self, c0: &Fp2, c1: &Fp2, c4: &Fp2) -> Fp12 { + let aa = self.c0.mul_by_01(c0, c1); + let bb = self.c1.mul_by_1(c4); + let o = c1 + c4; + let c1 = self.c1 + self.c0; + let c1 = c1.mul_by_01(c0, &o); + let c1 = c1 - aa - bb; + let c0 = bb; + let c0 = c0.mul_by_nonresidue(); + let c0 = c0 + aa; + + Fp12 { c0, c1 } + } + + #[inline(always)] + pub fn is_zero(&self) -> Choice { + self.c0.is_zero() & self.c1.is_zero() + } + + #[inline(always)] + pub fn conjugate(&self) -> Self { + Fp12 { + c0: self.c0, + c1: -self.c1, + } + } + + /// Raises this element to p. + #[inline(always)] + pub fn frobenius_map(&self) -> Self { + let c0 = self.c0.frobenius_map(); + let c1 = self.c1.frobenius_map(); + + // c1 = c1 * (u + 1)^((p - 1) / 6) + let c1 = c1 + * Fp6::from(Fp2 { + c0: Fp::from_raw_unchecked([ + 0x0708_9552_b319_d465, + 0xc669_5f92_b50a_8313, + 0x97e8_3ccc_d117_228f, + 0xa35b_aeca_b2dc_29ee, + 0x1ce3_93ea_5daa_ce4d, + 0x08f2_220f_b0fb_66eb, + ]), + c1: Fp::from_raw_unchecked([ + 0xb2f6_6aad_4ce5_d646, + 0x5842_a06b_fc49_7cec, + 0xcf48_95d4_2599_d394, + 0xc11b_9cba_40a8_e8d0, + 0x2e38_13cb_e5a0_de89, + 0x110e_efda_8884_7faf, + ]), + }); + + Fp12 { c0, c1 } + } + + #[inline] + pub fn square(&self) -> Self { + let ab = self.c0 * self.c1; + let c0c1 = self.c0 + self.c1; + let c0 = self.c1.mul_by_nonresidue(); + let c0 = c0 + self.c0; + let c0 = c0 * c0c1; + let c0 = c0 - ab; + let c1 = ab + ab; + let c0 = c0 - ab.mul_by_nonresidue(); + + Fp12 { c0, c1 } + } + + pub fn invert(&self) -> CtOption { + (self.c0.square() - self.c1.square().mul_by_nonresidue()) + .invert() + .map(|t| Fp12 { + c0: self.c0 * t, + c1: self.c1 * -t, + }) + } +} + +impl<'a, 'b> Mul<&'b Fp12> for &'a Fp12 { + type Output = Fp12; + + #[inline] + fn mul(self, other: &'b Fp12) -> Self::Output { + let aa = self.c0 * other.c0; + let bb = self.c1 * other.c1; + let o = other.c0 + other.c1; + let c1 = self.c1 + self.c0; + let c1 = c1 * o; + let c1 = c1 - aa; + let c1 = c1 - bb; + let c0 = bb.mul_by_nonresidue(); + let c0 = c0 + aa; + + Fp12 { c0, c1 } + } +} + +impl<'a, 'b> Add<&'b Fp12> for &'a Fp12 { + type Output = Fp12; + + #[inline] + fn add(self, rhs: &'b Fp12) -> Self::Output { + Fp12 { + c0: self.c0 + rhs.c0, + c1: self.c1 + rhs.c1, + } + } +} + +impl<'a> Neg for &'a Fp12 { + type Output = Fp12; + + #[inline] + fn neg(self) -> Self::Output { + Fp12 { + c0: -self.c0, + c1: -self.c1, + } + } +} + +impl Neg for Fp12 { + type Output = Fp12; + + #[inline] + fn neg(self) -> Self::Output { + -&self + } +} + +impl<'a, 'b> Sub<&'b Fp12> for &'a Fp12 { + type Output = Fp12; + + #[inline] + fn sub(self, rhs: &'b Fp12) -> Self::Output { + Fp12 { + c0: self.c0 - rhs.c0, + c1: self.c1 - rhs.c1, + } + } +} + +impl_binops_additive!(Fp12, Fp12); +impl_binops_multiplicative!(Fp12, Fp12); + +#[test] +fn test_arithmetic() { + use crate::fp::*; + use crate::fp2::*; + + let a = Fp12 { + c0: Fp6 { + c0: Fp2 { + c0: Fp::from_raw_unchecked([ + 0x47f9_cb98_b1b8_2d58, + 0x5fe9_11eb_a3aa_1d9d, + 0x96bf_1b5f_4dd8_1db3, + 0x8100_d27c_c925_9f5b, + 0xafa2_0b96_7464_0eab, + 0x09bb_cea7_d8d9_497d, + ]), + c1: Fp::from_raw_unchecked([ + 0x0303_cb98_b166_2daa, + 0xd931_10aa_0a62_1d5a, + 0xbfa9_820c_5be4_a468, + 0x0ba3_643e_cb05_a348, + 0xdc35_34bb_1f1c_25a6, + 0x06c3_05bb_19c0_e1c1, + ]), + }, + c1: Fp2 { + c0: Fp::from_raw_unchecked([ + 0x46f9_cb98_b162_d858, + 0x0be9_109c_f7aa_1d57, + 0xc791_bc55_fece_41d2, + 0xf84c_5770_4e38_5ec2, + 0xcb49_c1d9_c010_e60f, + 0x0acd_b8e1_58bf_e3c8, + ]), + c1: Fp::from_raw_unchecked([ + 0x8aef_cb98_b15f_8306, + 0x3ea1_108f_e4f2_1d54, + 0xcf79_f69f_a1b7_df3b, + 0xe4f5_4aa1_d16b_1a3c, + 0xba5e_4ef8_6105_a679, + 0x0ed8_6c07_97be_e5cf, + ]), + }, + c2: Fp2 { + c0: Fp::from_raw_unchecked([ + 0xcee5_cb98_b15c_2db4, + 0x7159_1082_d23a_1d51, + 0xd762_30e9_44a1_7ca4, + 0xd19e_3dd3_549d_d5b6, + 0xa972_dc17_01fa_66e3, + 0x12e3_1f2d_d6bd_e7d6, + ]), + c1: Fp::from_raw_unchecked([ + 0xad2a_cb98_b173_2d9d, + 0x2cfd_10dd_0696_1d64, + 0x0739_6b86_c6ef_24e8, + 0xbd76_e2fd_b1bf_c820, + 0x6afe_a7f6_de94_d0d5, + 0x1099_4b0c_5744_c040, + ]), + }, + }, + c1: Fp6 { + c0: Fp2 { + c0: Fp::from_raw_unchecked([ + 0x47f9_cb98_b1b8_2d58, + 0x5fe9_11eb_a3aa_1d9d, + 0x96bf_1b5f_4dd8_1db3, + 0x8100_d27c_c925_9f5b, + 0xafa2_0b96_7464_0eab, + 0x09bb_cea7_d8d9_497d, + ]), + c1: Fp::from_raw_unchecked([ + 0x0303_cb98_b166_2daa, + 0xd931_10aa_0a62_1d5a, + 0xbfa9_820c_5be4_a468, + 0x0ba3_643e_cb05_a348, + 0xdc35_34bb_1f1c_25a6, + 0x06c3_05bb_19c0_e1c1, + ]), + }, + c1: Fp2 { + c0: Fp::from_raw_unchecked([ + 0x46f9_cb98_b162_d858, + 0x0be9_109c_f7aa_1d57, + 0xc791_bc55_fece_41d2, + 0xf84c_5770_4e38_5ec2, + 0xcb49_c1d9_c010_e60f, + 0x0acd_b8e1_58bf_e3c8, + ]), + c1: Fp::from_raw_unchecked([ + 0x8aef_cb98_b15f_8306, + 0x3ea1_108f_e4f2_1d54, + 0xcf79_f69f_a1b7_df3b, + 0xe4f5_4aa1_d16b_1a3c, + 0xba5e_4ef8_6105_a679, + 0x0ed8_6c07_97be_e5cf, + ]), + }, + c2: Fp2 { + c0: Fp::from_raw_unchecked([ + 0xcee5_cb98_b15c_2db4, + 0x7159_1082_d23a_1d51, + 0xd762_30e9_44a1_7ca4, + 0xd19e_3dd3_549d_d5b6, + 0xa972_dc17_01fa_66e3, + 0x12e3_1f2d_d6bd_e7d6, + ]), + c1: Fp::from_raw_unchecked([ + 0xad2a_cb98_b173_2d9d, + 0x2cfd_10dd_0696_1d64, + 0x0739_6b86_c6ef_24e8, + 0xbd76_e2fd_b1bf_c820, + 0x6afe_a7f6_de94_d0d5, + 0x1099_4b0c_5744_c040, + ]), + }, + }, + }; + + let b = Fp12 { + c0: Fp6 { + c0: Fp2 { + c0: Fp::from_raw_unchecked([ + 0x47f9_cb98_b1b8_2d58, + 0x5fe9_11eb_a3aa_1d9d, + 0x96bf_1b5f_4dd8_1db3, + 0x8100_d272_c925_9f5b, + 0xafa2_0b96_7464_0eab, + 0x09bb_cea7_d8d9_497d, + ]), + c1: Fp::from_raw_unchecked([ + 0x0303_cb98_b166_2daa, + 0xd931_10aa_0a62_1d5a, + 0xbfa9_820c_5be4_a468, + 0x0ba3_643e_cb05_a348, + 0xdc35_34bb_1f1c_25a6, + 0x06c3_05bb_19c0_e1c1, + ]), + }, + c1: Fp2 { + c0: Fp::from_raw_unchecked([ + 0x46f9_cb98_b162_d858, + 0x0be9_109c_f7aa_1d57, + 0xc791_bc55_fece_41d2, + 0xf84c_5770_4e38_5ec2, + 0xcb49_c1d9_c010_e60f, + 0x0acd_b8e1_58bf_e348, + ]), + c1: Fp::from_raw_unchecked([ + 0x8aef_cb98_b15f_8306, + 0x3ea1_108f_e4f2_1d54, + 0xcf79_f69f_a1b7_df3b, + 0xe4f5_4aa1_d16b_1a3c, + 0xba5e_4ef8_6105_a679, + 0x0ed8_6c07_97be_e5cf, + ]), + }, + c2: Fp2 { + c0: Fp::from_raw_unchecked([ + 0xcee5_cb98_b15c_2db4, + 0x7159_1082_d23a_1d51, + 0xd762_30e9_44a1_7ca4, + 0xd19e_3dd3_549d_d5b6, + 0xa972_dc17_01fa_66e3, + 0x12e3_1f2d_d6bd_e7d6, + ]), + c1: Fp::from_raw_unchecked([ + 0xad2a_cb98_b173_2d9d, + 0x2cfd_10dd_0696_1d64, + 0x0739_6b86_c6ef_24e8, + 0xbd76_e2fd_b1bf_c820, + 0x6afe_a7f6_de94_d0d5, + 0x1099_4b0c_5744_c040, + ]), + }, + }, + c1: Fp6 { + c0: Fp2 { + c0: Fp::from_raw_unchecked([ + 0x47f9_cb98_b1b8_2d58, + 0x5fe9_11eb_a3aa_1d9d, + 0x96bf_1b5f_4dd2_1db3, + 0x8100_d27c_c925_9f5b, + 0xafa2_0b96_7464_0eab, + 0x09bb_cea7_d8d9_497d, + ]), + c1: Fp::from_raw_unchecked([ + 0x0303_cb98_b166_2daa, + 0xd931_10aa_0a62_1d5a, + 0xbfa9_820c_5be4_a468, + 0x0ba3_643e_cb05_a348, + 0xdc35_34bb_1f1c_25a6, + 0x06c3_05bb_19c0_e1c1, + ]), + }, + c1: Fp2 { + c0: Fp::from_raw_unchecked([ + 0x46f9_cb98_b162_d858, + 0x0be9_109c_f7aa_1d57, + 0xc791_bc55_fece_41d2, + 0xf84c_5770_4e38_5ec2, + 0xcb49_c1d9_c010_e60f, + 0x0acd_b8e1_58bf_e3c8, + ]), + c1: Fp::from_raw_unchecked([ + 0x8aef_cb98_b15f_8306, + 0x3ea1_108f_e4f2_1d54, + 0xcf79_f69f_a117_df3b, + 0xe4f5_4aa1_d16b_1a3c, + 0xba5e_4ef8_6105_a679, + 0x0ed8_6c07_97be_e5cf, + ]), + }, + c2: Fp2 { + c0: Fp::from_raw_unchecked([ + 0xcee5_cb98_b15c_2db4, + 0x7159_1082_d23a_1d51, + 0xd762_30e9_44a1_7ca4, + 0xd19e_3dd3_549d_d5b6, + 0xa972_dc17_01fa_66e3, + 0x12e3_1f2d_d6bd_e7d6, + ]), + c1: Fp::from_raw_unchecked([ + 0xad2a_cb98_b173_2d9d, + 0x2cfd_10dd_0696_1d64, + 0x0739_6b86_c6ef_24e8, + 0xbd76_e2fd_b1bf_c820, + 0x6afe_a7f6_de94_d0d5, + 0x1099_4b0c_5744_c040, + ]), + }, + }, + }; + + let c = Fp12 { + c0: Fp6 { + c0: Fp2 { + c0: Fp::from_raw_unchecked([ + 0x47f9_cb98_71b8_2d58, + 0x5fe9_11eb_a3aa_1d9d, + 0x96bf_1b5f_4dd8_1db3, + 0x8100_d27c_c925_9f5b, + 0xafa2_0b96_7464_0eab, + 0x09bb_cea7_d8d9_497d, + ]), + c1: Fp::from_raw_unchecked([ + 0x0303_cb98_b166_2daa, + 0xd931_10aa_0a62_1d5a, + 0xbfa9_820c_5be4_a468, + 0x0ba3_643e_cb05_a348, + 0xdc35_34bb_1f1c_25a6, + 0x06c3_05bb_19c0_e1c1, + ]), + }, + c1: Fp2 { + c0: Fp::from_raw_unchecked([ + 0x46f9_cb98_b162_d858, + 0x0be9_109c_f7aa_1d57, + 0x7791_bc55_fece_41d2, + 0xf84c_5770_4e38_5ec2, + 0xcb49_c1d9_c010_e60f, + 0x0acd_b8e1_58bf_e3c8, + ]), + c1: Fp::from_raw_unchecked([ + 0x8aef_cb98_b15f_8306, + 0x3ea1_108f_e4f2_1d54, + 0xcf79_f69f_a1b7_df3b, + 0xe4f5_4aa1_d16b_133c, + 0xba5e_4ef8_6105_a679, + 0x0ed8_6c07_97be_e5cf, + ]), + }, + c2: Fp2 { + c0: Fp::from_raw_unchecked([ + 0xcee5_cb98_b15c_2db4, + 0x7159_1082_d23a_1d51, + 0xd762_40e9_44a1_7ca4, + 0xd19e_3dd3_549d_d5b6, + 0xa972_dc17_01fa_66e3, + 0x12e3_1f2d_d6bd_e7d6, + ]), + c1: Fp::from_raw_unchecked([ + 0xad2a_cb98_b173_2d9d, + 0x2cfd_10dd_0696_1d64, + 0x0739_6b86_c6ef_24e8, + 0xbd76_e2fd_b1bf_c820, + 0x6afe_a7f6_de94_d0d5, + 0x1099_4b0c_1744_c040, + ]), + }, + }, + c1: Fp6 { + c0: Fp2 { + c0: Fp::from_raw_unchecked([ + 0x47f9_cb98_b1b8_2d58, + 0x5fe9_11eb_a3aa_1d9d, + 0x96bf_1b5f_4dd8_1db3, + 0x8100_d27c_c925_9f5b, + 0xafa2_0b96_7464_0eab, + 0x09bb_cea7_d8d9_497d, + ]), + c1: Fp::from_raw_unchecked([ + 0x0303_cb98_b166_2daa, + 0xd931_10aa_0a62_1d5a, + 0xbfa9_820c_5be4_a468, + 0x0ba3_643e_cb05_a348, + 0xdc35_34bb_1f1c_25a6, + 0x06c3_05bb_19c0_e1c1, + ]), + }, + c1: Fp2 { + c0: Fp::from_raw_unchecked([ + 0x46f9_cb98_b162_d858, + 0x0be9_109c_f7aa_1d57, + 0xc791_bc55_fece_41d2, + 0xf84c_5770_4e38_5ec2, + 0xcb49_c1d3_c010_e60f, + 0x0acd_b8e1_58bf_e3c8, + ]), + c1: Fp::from_raw_unchecked([ + 0x8aef_cb98_b15f_8306, + 0x3ea1_108f_e4f2_1d54, + 0xcf79_f69f_a1b7_df3b, + 0xe4f5_4aa1_d16b_1a3c, + 0xba5e_4ef8_6105_a679, + 0x0ed8_6c07_97be_e5cf, + ]), + }, + c2: Fp2 { + c0: Fp::from_raw_unchecked([ + 0xcee5_cb98_b15c_2db4, + 0x7159_1082_d23a_1d51, + 0xd762_30e9_44a1_7ca4, + 0xd19e_3dd3_549d_d5b6, + 0xa972_dc17_01fa_66e3, + 0x12e3_1f2d_d6bd_e7d6, + ]), + c1: Fp::from_raw_unchecked([ + 0xad2a_cb98_b173_2d9d, + 0x2cfd_10dd_0696_1d64, + 0x0739_6b86_c6ef_24e8, + 0xbd76_e2fd_b1bf_c820, + 0x6afe_a7f6_de94_d0d5, + 0x1099_4b0c_5744_1040, + ]), + }, + }, + }; + + // because a and b and c are similar to each other and + // I was lazy, this is just some arbitrary way to make + // them a little more different + let a = a.square().invert().unwrap().square() + c; + let b = b.square().invert().unwrap().square() + a; + let c = c.square().invert().unwrap().square() + b; + + assert_eq!(a.square(), a * a); + assert_eq!(b.square(), b * b); + assert_eq!(c.square(), c * c); + + assert_eq!((a + b) * c.square(), (c * c * a) + (c * c * b)); + + assert_eq!( + a.invert().unwrap() * b.invert().unwrap(), + (a * b).invert().unwrap() + ); + assert_eq!(a.invert().unwrap() * a, Fp12::one()); + + assert!(a != a.frobenius_map()); + assert_eq!( + a, + a.frobenius_map() + .frobenius_map() + .frobenius_map() + .frobenius_map() + .frobenius_map() + .frobenius_map() + .frobenius_map() + .frobenius_map() + .frobenius_map() + .frobenius_map() + .frobenius_map() + .frobenius_map() + ); +} + +#[cfg(feature = "zeroize")] +#[test] +fn test_zeroize() { + use zeroize::Zeroize; + + let mut a = Fp12::one(); + a.zeroize(); + assert!(bool::from(a.is_zero())); +} diff --git a/constantine/bls12_381/src/fp2.rs b/constantine/bls12_381/src/fp2.rs new file mode 100644 index 000000000..6d99d639f --- /dev/null +++ b/constantine/bls12_381/src/fp2.rs @@ -0,0 +1,898 @@ +//! This module implements arithmetic over the quadratic extension field Fp2. +#![allow(clippy::all)] + +use core::fmt; +use core::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}; +use rand_core::RngCore; +use subtle::{Choice, ConditionallySelectable, ConstantTimeEq, CtOption}; + +use crate::fp::Fp; + +#[derive(Copy, Clone)] +pub struct Fp2 { + pub c0: Fp, + pub c1: Fp, +} + +impl fmt::Debug for Fp2 { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?} + {:?}*u", self.c0, self.c1) + } +} + +impl Default for Fp2 { + fn default() -> Self { + Fp2::zero() + } +} + +#[cfg(feature = "zeroize")] +impl zeroize::DefaultIsZeroes for Fp2 {} + +impl From for Fp2 { + fn from(f: Fp) -> Fp2 { + Fp2 { + c0: f, + c1: Fp::zero(), + } + } +} + +impl ConstantTimeEq for Fp2 { + fn ct_eq(&self, other: &Self) -> Choice { + self.c0.ct_eq(&other.c0) & self.c1.ct_eq(&other.c1) + } +} + +impl Eq for Fp2 {} +impl PartialEq for Fp2 { + #[inline] + fn eq(&self, other: &Self) -> bool { + bool::from(self.ct_eq(other)) + } +} + +impl ConditionallySelectable for Fp2 { + fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self { + Fp2 { + c0: Fp::conditional_select(&a.c0, &b.c0, choice), + c1: Fp::conditional_select(&a.c1, &b.c1, choice), + } + } +} + +impl<'a> Neg for &'a Fp2 { + type Output = Fp2; + + #[inline] + fn neg(self) -> Fp2 { + self.neg() + } +} + +impl Neg for Fp2 { + type Output = Fp2; + + #[inline] + fn neg(self) -> Fp2 { + -&self + } +} + +impl<'a, 'b> Sub<&'b Fp2> for &'a Fp2 { + type Output = Fp2; + + #[inline] + fn sub(self, rhs: &'b Fp2) -> Fp2 { + self.sub(rhs) + } +} + +impl<'a, 'b> Add<&'b Fp2> for &'a Fp2 { + type Output = Fp2; + + #[inline] + fn add(self, rhs: &'b Fp2) -> Fp2 { + self.add(rhs) + } +} + +impl<'a, 'b> Mul<&'b Fp2> for &'a Fp2 { + type Output = Fp2; + + #[inline] + fn mul(self, rhs: &'b Fp2) -> Fp2 { + self.mul(rhs) + } +} + +impl_binops_additive!(Fp2, Fp2); +impl_binops_multiplicative!(Fp2, Fp2); + +impl Fp2 { + #[inline] + pub const fn zero() -> Fp2 { + Fp2 { + c0: Fp::zero(), + c1: Fp::zero(), + } + } + + #[inline] + pub const fn one() -> Fp2 { + Fp2 { + c0: Fp::one(), + c1: Fp::zero(), + } + } + + pub fn is_zero(&self) -> Choice { + self.c0.is_zero() & self.c1.is_zero() + } + + pub(crate) fn random(mut rng: impl RngCore) -> Fp2 { + Fp2 { + c0: Fp::random(&mut rng), + c1: Fp::random(&mut rng), + } + } + + /// Raises this element to p. + #[inline(always)] + pub fn frobenius_map(&self) -> Self { + // This is always just a conjugation. If you're curious why, here's + // an article about it: https://alicebob.cryptoland.net/the-frobenius-endomorphism-with-finite-fields/ + self.conjugate() + } + + #[inline(always)] + pub fn conjugate(&self) -> Self { + Fp2 { + c0: self.c0, + c1: -self.c1, + } + } + + #[inline(always)] + pub fn mul_by_nonresidue(&self) -> Fp2 { + // Multiply a + bu by u + 1, getting + // au + a + bu^2 + bu + // and because u^2 = -1, we get + // (a - b) + (a + b)u + + Fp2 { + c0: self.c0 - self.c1, + c1: self.c0 + self.c1, + } + } + + /// Returns whether or not this element is strictly lexicographically + /// larger than its negation. + #[inline] + pub fn lexicographically_largest(&self) -> Choice { + // If this element's c1 coefficient is lexicographically largest + // then it is lexicographically largest. Otherwise, in the event + // the c1 coefficient is zero and the c0 coefficient is + // lexicographically largest, then this element is lexicographically + // largest. + + self.c1.lexicographically_largest() + | (self.c1.is_zero() & self.c0.lexicographically_largest()) + } + + pub const fn square(&self) -> Fp2 { + // Complex squaring: + // + // v0 = c0 * c1 + // c0' = (c0 + c1) * (c0 + \beta*c1) - v0 - \beta * v0 + // c1' = 2 * v0 + // + // In BLS12-381's F_{p^2}, our \beta is -1 so we + // can modify this formula: + // + // c0' = (c0 + c1) * (c0 - c1) + // c1' = 2 * c0 * c1 + + let a = (&self.c0).add(&self.c1); + let b = (&self.c0).sub(&self.c1); + let c = (&self.c0).add(&self.c0); + + Fp2 { + c0: (&a).mul(&b), + c1: (&c).mul(&self.c1), + } + } + + pub fn mul(&self, rhs: &Fp2) -> Fp2 { + // F_{p^2} x F_{p^2} multiplication implemented with operand scanning (schoolbook) + // computes the result as: + // + // aยทb = (a_0 b_0 + a_1 b_1 ฮฒ) + (a_0 b_1 + a_1 b_0)i + // + // In BLS12-381's F_{p^2}, our ฮฒ is -1, so the resulting F_{p^2} element is: + // + // c_0 = a_0 b_0 - a_1 b_1 + // c_1 = a_0 b_1 + a_1 b_0 + // + // Each of these is a "sum of products", which we can compute efficiently. + + Fp2 { + c0: Fp::sum_of_products([self.c0, -self.c1], [rhs.c0, rhs.c1]), + c1: Fp::sum_of_products([self.c0, self.c1], [rhs.c1, rhs.c0]), + } + } + + pub const fn add(&self, rhs: &Fp2) -> Fp2 { + Fp2 { + c0: (&self.c0).add(&rhs.c0), + c1: (&self.c1).add(&rhs.c1), + } + } + + pub const fn sub(&self, rhs: &Fp2) -> Fp2 { + Fp2 { + c0: (&self.c0).sub(&rhs.c0), + c1: (&self.c1).sub(&rhs.c1), + } + } + + pub const fn neg(&self) -> Fp2 { + Fp2 { + c0: (&self.c0).neg(), + c1: (&self.c1).neg(), + } + } + + pub fn sqrt(&self) -> CtOption { + // Algorithm 9, https://eprint.iacr.org/2012/685.pdf + // with constant time modifications. + + CtOption::new(Fp2::zero(), self.is_zero()).or_else(|| { + // a1 = self^((p - 3) / 4) + let a1 = self.pow_vartime(&[ + 0xee7f_bfff_ffff_eaaa, + 0x07aa_ffff_ac54_ffff, + 0xd9cc_34a8_3dac_3d89, + 0xd91d_d2e1_3ce1_44af, + 0x92c6_e9ed_90d2_eb35, + 0x0680_447a_8e5f_f9a6, + ]); + + // alpha = a1^2 * self = self^((p - 3) / 2 + 1) = self^((p - 1) / 2) + let alpha = a1.square() * self; + + // x0 = self^((p + 1) / 4) + let x0 = a1 * self; + + // In the event that alpha = -1, the element is order p - 1 and so + // we're just trying to get the square of an element of the subfield + // Fp. This is given by x0 * u, since u = sqrt(-1). Since the element + // x0 = a + bu has b = 0, the solution is therefore au. + CtOption::new( + Fp2 { + c0: -x0.c1, + c1: x0.c0, + }, + alpha.ct_eq(&(&Fp2::one()).neg()), + ) + // Otherwise, the correct solution is (1 + alpha)^((q - 1) // 2) * x0 + .or_else(|| { + CtOption::new( + (alpha + Fp2::one()).pow_vartime(&[ + 0xdcff_7fff_ffff_d555, + 0x0f55_ffff_58a9_ffff, + 0xb398_6950_7b58_7b12, + 0xb23b_a5c2_79c2_895f, + 0x258d_d3db_21a5_d66b, + 0x0d00_88f5_1cbf_f34d, + ]) * x0, + Choice::from(1), + ) + }) + // Only return the result if it's really the square root (and so + // self is actually quadratic nonresidue) + .and_then(|sqrt| CtOption::new(sqrt, sqrt.square().ct_eq(self))) + }) + } + + /// Computes the multiplicative inverse of this field + /// element, returning None in the case that this element + /// is zero. + pub fn invert(&self) -> CtOption { + // We wish to find the multiplicative inverse of a nonzero + // element a + bu in Fp2. We leverage an identity + // + // (a + bu)(a - bu) = a^2 + b^2 + // + // which holds because u^2 = -1. This can be rewritten as + // + // (a + bu)(a - bu)/(a^2 + b^2) = 1 + // + // because a^2 + b^2 = 0 has no nonzero solutions for (a, b). + // This gives that (a - bu)/(a^2 + b^2) is the inverse + // of (a + bu). Importantly, this can be computing using + // only a single inversion in Fp. + + (self.c0.square() + self.c1.square()).invert().map(|t| Fp2 { + c0: self.c0 * t, + c1: self.c1 * -t, + }) + } + + /// Although this is labeled "vartime", it is only + /// variable time with respect to the exponent. It + /// is also not exposed in the public API. + pub fn pow_vartime(&self, by: &[u64; 6]) -> Self { + let mut res = Self::one(); + for e in by.iter().rev() { + for i in (0..64).rev() { + res = res.square(); + + if ((*e >> i) & 1) == 1 { + res *= self; + } + } + } + res + } + + /// Vartime exponentiation for larger exponents, only + /// used in testing and not exposed through the public API. + #[cfg(all(test, feature = "experimental"))] + pub(crate) fn pow_vartime_extended(&self, by: &[u64]) -> Self { + let mut res = Self::one(); + for e in by.iter().rev() { + for i in (0..64).rev() { + res = res.square(); + + if ((*e >> i) & 1) == 1 { + res *= self; + } + } + } + res + } +} + +#[test] +fn test_conditional_selection() { + let a = Fp2 { + c0: Fp::from_raw_unchecked([1, 2, 3, 4, 5, 6]), + c1: Fp::from_raw_unchecked([7, 8, 9, 10, 11, 12]), + }; + let b = Fp2 { + c0: Fp::from_raw_unchecked([13, 14, 15, 16, 17, 18]), + c1: Fp::from_raw_unchecked([19, 20, 21, 22, 23, 24]), + }; + + assert_eq!( + ConditionallySelectable::conditional_select(&a, &b, Choice::from(0u8)), + a + ); + assert_eq!( + ConditionallySelectable::conditional_select(&a, &b, Choice::from(1u8)), + b + ); +} + +#[test] +fn test_equality() { + fn is_equal(a: &Fp2, b: &Fp2) -> bool { + let eq = a == b; + let ct_eq = a.ct_eq(&b); + + assert_eq!(eq, bool::from(ct_eq)); + + eq + } + + assert!(is_equal( + &Fp2 { + c0: Fp::from_raw_unchecked([1, 2, 3, 4, 5, 6]), + c1: Fp::from_raw_unchecked([7, 8, 9, 10, 11, 12]), + }, + &Fp2 { + c0: Fp::from_raw_unchecked([1, 2, 3, 4, 5, 6]), + c1: Fp::from_raw_unchecked([7, 8, 9, 10, 11, 12]), + } + )); + + assert!(!is_equal( + &Fp2 { + c0: Fp::from_raw_unchecked([2, 2, 3, 4, 5, 6]), + c1: Fp::from_raw_unchecked([7, 8, 9, 10, 11, 12]), + }, + &Fp2 { + c0: Fp::from_raw_unchecked([1, 2, 3, 4, 5, 6]), + c1: Fp::from_raw_unchecked([7, 8, 9, 10, 11, 12]), + } + )); + + assert!(!is_equal( + &Fp2 { + c0: Fp::from_raw_unchecked([1, 2, 3, 4, 5, 6]), + c1: Fp::from_raw_unchecked([2, 8, 9, 10, 11, 12]), + }, + &Fp2 { + c0: Fp::from_raw_unchecked([1, 2, 3, 4, 5, 6]), + c1: Fp::from_raw_unchecked([7, 8, 9, 10, 11, 12]), + } + )); +} + +#[test] +fn test_squaring() { + let a = Fp2 { + c0: Fp::from_raw_unchecked([ + 0xc9a2_1831_63ee_70d4, + 0xbc37_70a7_196b_5c91, + 0xa247_f8c1_304c_5f44, + 0xb01f_c2a3_726c_80b5, + 0xe1d2_93e5_bbd9_19c9, + 0x04b7_8e80_020e_f2ca, + ]), + c1: Fp::from_raw_unchecked([ + 0x952e_a446_0462_618f, + 0x238d_5edd_f025_c62f, + 0xf6c9_4b01_2ea9_2e72, + 0x03ce_24ea_c1c9_3808, + 0x0559_50f9_45da_483c, + 0x010a_768d_0df4_eabc, + ]), + }; + let b = Fp2 { + c0: Fp::from_raw_unchecked([ + 0xa1e0_9175_a4d2_c1fe, + 0x8b33_acfc_204e_ff12, + 0xe244_15a1_1b45_6e42, + 0x61d9_96b1_b6ee_1936, + 0x1164_dbe8_667c_853c, + 0x0788_557a_cc7d_9c79, + ]), + c1: Fp::from_raw_unchecked([ + 0xda6a_87cc_6f48_fa36, + 0x0fc7_b488_277c_1903, + 0x9445_ac4a_dc44_8187, + 0x0261_6d5b_c909_9209, + 0xdbed_4677_2db5_8d48, + 0x11b9_4d50_76c7_b7b1, + ]), + }; + + assert_eq!(a.square(), b); +} + +#[test] +fn test_multiplication() { + let a = Fp2 { + c0: Fp::from_raw_unchecked([ + 0xc9a2_1831_63ee_70d4, + 0xbc37_70a7_196b_5c91, + 0xa247_f8c1_304c_5f44, + 0xb01f_c2a3_726c_80b5, + 0xe1d2_93e5_bbd9_19c9, + 0x04b7_8e80_020e_f2ca, + ]), + c1: Fp::from_raw_unchecked([ + 0x952e_a446_0462_618f, + 0x238d_5edd_f025_c62f, + 0xf6c9_4b01_2ea9_2e72, + 0x03ce_24ea_c1c9_3808, + 0x0559_50f9_45da_483c, + 0x010a_768d_0df4_eabc, + ]), + }; + let b = Fp2 { + c0: Fp::from_raw_unchecked([ + 0xa1e0_9175_a4d2_c1fe, + 0x8b33_acfc_204e_ff12, + 0xe244_15a1_1b45_6e42, + 0x61d9_96b1_b6ee_1936, + 0x1164_dbe8_667c_853c, + 0x0788_557a_cc7d_9c79, + ]), + c1: Fp::from_raw_unchecked([ + 0xda6a_87cc_6f48_fa36, + 0x0fc7_b488_277c_1903, + 0x9445_ac4a_dc44_8187, + 0x0261_6d5b_c909_9209, + 0xdbed_4677_2db5_8d48, + 0x11b9_4d50_76c7_b7b1, + ]), + }; + let c = Fp2 { + c0: Fp::from_raw_unchecked([ + 0xf597_483e_27b4_e0f7, + 0x610f_badf_811d_ae5f, + 0x8432_af91_7714_327a, + 0x6a9a_9603_cf88_f09e, + 0xf05a_7bf8_bad0_eb01, + 0x0954_9131_c003_ffae, + ]), + c1: Fp::from_raw_unchecked([ + 0x963b_02d0_f93d_37cd, + 0xc95c_e1cd_b30a_73d4, + 0x3087_25fa_3126_f9b8, + 0x56da_3c16_7fab_0d50, + 0x6b50_86b5_f4b6_d6af, + 0x09c3_9f06_2f18_e9f2, + ]), + }; + + assert_eq!(a * b, c); +} + +#[test] +fn test_addition() { + let a = Fp2 { + c0: Fp::from_raw_unchecked([ + 0xc9a2_1831_63ee_70d4, + 0xbc37_70a7_196b_5c91, + 0xa247_f8c1_304c_5f44, + 0xb01f_c2a3_726c_80b5, + 0xe1d2_93e5_bbd9_19c9, + 0x04b7_8e80_020e_f2ca, + ]), + c1: Fp::from_raw_unchecked([ + 0x952e_a446_0462_618f, + 0x238d_5edd_f025_c62f, + 0xf6c9_4b01_2ea9_2e72, + 0x03ce_24ea_c1c9_3808, + 0x0559_50f9_45da_483c, + 0x010a_768d_0df4_eabc, + ]), + }; + let b = Fp2 { + c0: Fp::from_raw_unchecked([ + 0xa1e0_9175_a4d2_c1fe, + 0x8b33_acfc_204e_ff12, + 0xe244_15a1_1b45_6e42, + 0x61d9_96b1_b6ee_1936, + 0x1164_dbe8_667c_853c, + 0x0788_557a_cc7d_9c79, + ]), + c1: Fp::from_raw_unchecked([ + 0xda6a_87cc_6f48_fa36, + 0x0fc7_b488_277c_1903, + 0x9445_ac4a_dc44_8187, + 0x0261_6d5b_c909_9209, + 0xdbed_4677_2db5_8d48, + 0x11b9_4d50_76c7_b7b1, + ]), + }; + let c = Fp2 { + c0: Fp::from_raw_unchecked([ + 0x6b82_a9a7_08c1_32d2, + 0x476b_1da3_39ba_5ba4, + 0x848c_0e62_4b91_cd87, + 0x11f9_5955_295a_99ec, + 0xf337_6fce_2255_9f06, + 0x0c3f_e3fa_ce8c_8f43, + ]), + c1: Fp::from_raw_unchecked([ + 0x6f99_2c12_73ab_5bc5, + 0x3355_1366_17a1_df33, + 0x8b0e_f74c_0aed_aff9, + 0x062f_9246_8ad2_ca12, + 0xe146_9770_738f_d584, + 0x12c3_c3dd_84bc_a26d, + ]), + }; + + assert_eq!(a + b, c); +} + +#[test] +fn test_subtraction() { + let a = Fp2 { + c0: Fp::from_raw_unchecked([ + 0xc9a2_1831_63ee_70d4, + 0xbc37_70a7_196b_5c91, + 0xa247_f8c1_304c_5f44, + 0xb01f_c2a3_726c_80b5, + 0xe1d2_93e5_bbd9_19c9, + 0x04b7_8e80_020e_f2ca, + ]), + c1: Fp::from_raw_unchecked([ + 0x952e_a446_0462_618f, + 0x238d_5edd_f025_c62f, + 0xf6c9_4b01_2ea9_2e72, + 0x03ce_24ea_c1c9_3808, + 0x0559_50f9_45da_483c, + 0x010a_768d_0df4_eabc, + ]), + }; + let b = Fp2 { + c0: Fp::from_raw_unchecked([ + 0xa1e0_9175_a4d2_c1fe, + 0x8b33_acfc_204e_ff12, + 0xe244_15a1_1b45_6e42, + 0x61d9_96b1_b6ee_1936, + 0x1164_dbe8_667c_853c, + 0x0788_557a_cc7d_9c79, + ]), + c1: Fp::from_raw_unchecked([ + 0xda6a_87cc_6f48_fa36, + 0x0fc7_b488_277c_1903, + 0x9445_ac4a_dc44_8187, + 0x0261_6d5b_c909_9209, + 0xdbed_4677_2db5_8d48, + 0x11b9_4d50_76c7_b7b1, + ]), + }; + let c = Fp2 { + c0: Fp::from_raw_unchecked([ + 0xe1c0_86bb_bf1b_5981, + 0x4faf_c3a9_aa70_5d7e, + 0x2734_b5c1_0bb7_e726, + 0xb2bd_7776_af03_7a3e, + 0x1b89_5fb3_98a8_4164, + 0x1730_4aef_6f11_3cec, + ]), + c1: Fp::from_raw_unchecked([ + 0x74c3_1c79_9519_1204, + 0x3271_aa54_79fd_ad2b, + 0xc9b4_7157_4915_a30f, + 0x65e4_0313_ec44_b8be, + 0x7487_b238_5b70_67cb, + 0x0952_3b26_d0ad_19a4, + ]), + }; + + assert_eq!(a - b, c); +} + +#[test] +fn test_negation() { + let a = Fp2 { + c0: Fp::from_raw_unchecked([ + 0xc9a2_1831_63ee_70d4, + 0xbc37_70a7_196b_5c91, + 0xa247_f8c1_304c_5f44, + 0xb01f_c2a3_726c_80b5, + 0xe1d2_93e5_bbd9_19c9, + 0x04b7_8e80_020e_f2ca, + ]), + c1: Fp::from_raw_unchecked([ + 0x952e_a446_0462_618f, + 0x238d_5edd_f025_c62f, + 0xf6c9_4b01_2ea9_2e72, + 0x03ce_24ea_c1c9_3808, + 0x0559_50f9_45da_483c, + 0x010a_768d_0df4_eabc, + ]), + }; + let b = Fp2 { + c0: Fp::from_raw_unchecked([ + 0xf05c_e7ce_9c11_39d7, + 0x6274_8f57_97e8_a36d, + 0xc4e8_d9df_c664_96df, + 0xb457_88e1_8118_9209, + 0x6949_13d0_8772_930d, + 0x1549_836a_3770_f3cf, + ]), + c1: Fp::from_raw_unchecked([ + 0x24d0_5bb9_fb9d_491c, + 0xfb1e_a120_c12e_39d0, + 0x7067_879f_c807_c7b1, + 0x60a9_269a_31bb_dab6, + 0x45c2_56bc_fd71_649b, + 0x18f6_9b5d_2b8a_fbde, + ]), + }; + + assert_eq!(-a, b); +} + +#[test] +fn test_sqrt() { + // a = 1488924004771393321054797166853618474668089414631333405711627789629391903630694737978065425271543178763948256226639*u + 784063022264861764559335808165825052288770346101304131934508881646553551234697082295473567906267937225174620141295 + let a = Fp2 { + c0: Fp::from_raw_unchecked([ + 0x2bee_d146_27d7_f9e9, + 0xb661_4e06_660e_5dce, + 0x06c4_cc7c_2f91_d42c, + 0x996d_7847_4b7a_63cc, + 0xebae_bc4c_820d_574e, + 0x1886_5e12_d93f_d845, + ]), + c1: Fp::from_raw_unchecked([ + 0x7d82_8664_baf4_f566, + 0xd17e_6639_96ec_7339, + 0x679e_ad55_cb40_78d0, + 0xfe3b_2260_e001_ec28, + 0x3059_93d0_43d9_1b68, + 0x0626_f03c_0489_b72d, + ]), + }; + + assert_eq!(a.sqrt().unwrap().square(), a); + + // b = 5, which is a generator of the p - 1 order + // multiplicative subgroup + let b = Fp2 { + c0: Fp::from_raw_unchecked([ + 0x6631_0000_0010_5545, + 0x2114_0040_0eec_000d, + 0x3fa7_af30_c820_e316, + 0xc52a_8b8d_6387_695d, + 0x9fb4_e61d_1e83_eac5, + 0x005c_b922_afe8_4dc7, + ]), + c1: Fp::zero(), + }; + + assert_eq!(b.sqrt().unwrap().square(), b); + + // c = 25, which is a generator of the (p - 1) / 2 order + // multiplicative subgroup + let c = Fp2 { + c0: Fp::from_raw_unchecked([ + 0x44f6_0000_0051_ffae, + 0x86b8_0141_9948_0043, + 0xd715_9952_f1f3_794a, + 0x755d_6e3d_fe1f_fc12, + 0xd36c_d6db_5547_e905, + 0x02f8_c8ec_bf18_67bb, + ]), + c1: Fp::zero(), + }; + + assert_eq!(c.sqrt().unwrap().square(), c); + + // 2155129644831861015726826462986972654175647013268275306775721078997042729172900466542651176384766902407257452753362*u + 2796889544896299244102912275102369318775038861758288697415827248356648685135290329705805931514906495247464901062529 + // is nonsquare. + assert!(bool::from( + Fp2 { + c0: Fp::from_raw_unchecked([ + 0xc5fa_1bc8_fd00_d7f6, + 0x3830_ca45_4606_003b, + 0x2b28_7f11_04b1_02da, + 0xa7fb_30f2_8230_f23e, + 0x339c_db9e_e953_dbf0, + 0x0d78_ec51_d989_fc57, + ]), + c1: Fp::from_raw_unchecked([ + 0x27ec_4898_cf87_f613, + 0x9de1_394e_1abb_05a5, + 0x0947_f85d_c170_fc14, + 0x586f_bc69_6b61_14b7, + 0x2b34_75a4_077d_7169, + 0x13e1_c895_cc4b_6c22, + ]) + } + .sqrt() + .is_none() + )); +} + +#[test] +fn test_inversion() { + let a = Fp2 { + c0: Fp::from_raw_unchecked([ + 0x1128_ecad_6754_9455, + 0x9e7a_1cff_3a4e_a1a8, + 0xeb20_8d51_e08b_cf27, + 0xe98a_d408_11f5_fc2b, + 0x736c_3a59_232d_511d, + 0x10ac_d42d_29cf_cbb6, + ]), + c1: Fp::from_raw_unchecked([ + 0xd328_e37c_c2f5_8d41, + 0x948d_f085_8a60_5869, + 0x6032_f9d5_6f93_a573, + 0x2be4_83ef_3fff_dc87, + 0x30ef_61f8_8f48_3c2a, + 0x1333_f55a_3572_5be0, + ]), + }; + + let b = Fp2 { + c0: Fp::from_raw_unchecked([ + 0x0581_a133_3d4f_48a6, + 0x5824_2f6e_f074_8500, + 0x0292_c955_349e_6da5, + 0xba37_721d_dd95_fcd0, + 0x70d1_6790_3aa5_dfc5, + 0x1189_5e11_8b58_a9d5, + ]), + c1: Fp::from_raw_unchecked([ + 0x0eda_09d2_d7a8_5d17, + 0x8808_e137_a7d1_a2cf, + 0x43ae_2625_c1ff_21db, + 0xf85a_c9fd_f7a7_4c64, + 0x8fcc_dda5_b8da_9738, + 0x08e8_4f0c_b32c_d17d, + ]), + }; + + assert_eq!(a.invert().unwrap(), b); + + assert!(bool::from(Fp2::zero().invert().is_none())); +} + +#[test] +fn test_lexicographic_largest() { + assert!(!bool::from(Fp2::zero().lexicographically_largest())); + assert!(!bool::from(Fp2::one().lexicographically_largest())); + assert!(bool::from( + Fp2 { + c0: Fp::from_raw_unchecked([ + 0x1128_ecad_6754_9455, + 0x9e7a_1cff_3a4e_a1a8, + 0xeb20_8d51_e08b_cf27, + 0xe98a_d408_11f5_fc2b, + 0x736c_3a59_232d_511d, + 0x10ac_d42d_29cf_cbb6, + ]), + c1: Fp::from_raw_unchecked([ + 0xd328_e37c_c2f5_8d41, + 0x948d_f085_8a60_5869, + 0x6032_f9d5_6f93_a573, + 0x2be4_83ef_3fff_dc87, + 0x30ef_61f8_8f48_3c2a, + 0x1333_f55a_3572_5be0, + ]), + } + .lexicographically_largest() + )); + assert!(!bool::from( + Fp2 { + c0: -Fp::from_raw_unchecked([ + 0x1128_ecad_6754_9455, + 0x9e7a_1cff_3a4e_a1a8, + 0xeb20_8d51_e08b_cf27, + 0xe98a_d408_11f5_fc2b, + 0x736c_3a59_232d_511d, + 0x10ac_d42d_29cf_cbb6, + ]), + c1: -Fp::from_raw_unchecked([ + 0xd328_e37c_c2f5_8d41, + 0x948d_f085_8a60_5869, + 0x6032_f9d5_6f93_a573, + 0x2be4_83ef_3fff_dc87, + 0x30ef_61f8_8f48_3c2a, + 0x1333_f55a_3572_5be0, + ]), + } + .lexicographically_largest() + )); + assert!(!bool::from( + Fp2 { + c0: Fp::from_raw_unchecked([ + 0x1128_ecad_6754_9455, + 0x9e7a_1cff_3a4e_a1a8, + 0xeb20_8d51_e08b_cf27, + 0xe98a_d408_11f5_fc2b, + 0x736c_3a59_232d_511d, + 0x10ac_d42d_29cf_cbb6, + ]), + c1: Fp::zero(), + } + .lexicographically_largest() + )); + assert!(bool::from( + Fp2 { + c0: -Fp::from_raw_unchecked([ + 0x1128_ecad_6754_9455, + 0x9e7a_1cff_3a4e_a1a8, + 0xeb20_8d51_e08b_cf27, + 0xe98a_d408_11f5_fc2b, + 0x736c_3a59_232d_511d, + 0x10ac_d42d_29cf_cbb6, + ]), + c1: Fp::zero(), + } + .lexicographically_largest() + )); +} + +#[cfg(feature = "zeroize")] +#[test] +fn test_zeroize() { + use zeroize::Zeroize; + + let mut a = Fp2::one(); + a.zeroize(); + assert!(bool::from(a.is_zero())); +} diff --git a/constantine/bls12_381/src/fp6.rs b/constantine/bls12_381/src/fp6.rs new file mode 100644 index 000000000..554204220 --- /dev/null +++ b/constantine/bls12_381/src/fp6.rs @@ -0,0 +1,571 @@ +use crate::fp::*; +use crate::fp2::*; + +use core::fmt; +use core::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}; +use subtle::{Choice, ConditionallySelectable, ConstantTimeEq, CtOption}; + +#[cfg(feature = "pairings")] +use rand_core::RngCore; + +/// This represents an element $c_0 + c_1 v + c_2 v^2$ of $\mathbb{F}_{p^6} = \mathbb{F}_{p^2} / v^3 - u - 1$. +pub struct Fp6 { + pub c0: Fp2, + pub c1: Fp2, + pub c2: Fp2, +} + +impl From for Fp6 { + fn from(f: Fp) -> Fp6 { + Fp6 { + c0: Fp2::from(f), + c1: Fp2::zero(), + c2: Fp2::zero(), + } + } +} + +impl From for Fp6 { + fn from(f: Fp2) -> Fp6 { + Fp6 { + c0: f, + c1: Fp2::zero(), + c2: Fp2::zero(), + } + } +} + +impl PartialEq for Fp6 { + fn eq(&self, other: &Fp6) -> bool { + self.ct_eq(other).into() + } +} + +impl Copy for Fp6 {} +impl Clone for Fp6 { + #[inline] + fn clone(&self) -> Self { + *self + } +} + +impl Default for Fp6 { + fn default() -> Self { + Fp6::zero() + } +} + +#[cfg(feature = "zeroize")] +impl zeroize::DefaultIsZeroes for Fp6 {} + +impl fmt::Debug for Fp6 { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?} + ({:?})*v + ({:?})*v^2", self.c0, self.c1, self.c2) + } +} + +impl ConditionallySelectable for Fp6 { + #[inline(always)] + fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self { + Fp6 { + c0: Fp2::conditional_select(&a.c0, &b.c0, choice), + c1: Fp2::conditional_select(&a.c1, &b.c1, choice), + c2: Fp2::conditional_select(&a.c2, &b.c2, choice), + } + } +} + +impl ConstantTimeEq for Fp6 { + #[inline(always)] + fn ct_eq(&self, other: &Self) -> Choice { + self.c0.ct_eq(&other.c0) & self.c1.ct_eq(&other.c1) & self.c2.ct_eq(&other.c2) + } +} + +impl Fp6 { + #[inline] + pub fn zero() -> Self { + Fp6 { + c0: Fp2::zero(), + c1: Fp2::zero(), + c2: Fp2::zero(), + } + } + + #[inline] + pub fn one() -> Self { + Fp6 { + c0: Fp2::one(), + c1: Fp2::zero(), + c2: Fp2::zero(), + } + } + + #[cfg(feature = "pairings")] + pub(crate) fn random(mut rng: impl RngCore) -> Self { + Fp6 { + c0: Fp2::random(&mut rng), + c1: Fp2::random(&mut rng), + c2: Fp2::random(&mut rng), + } + } + + pub fn mul_by_1(&self, c1: &Fp2) -> Fp6 { + Fp6 { + c0: (self.c2 * c1).mul_by_nonresidue(), + c1: self.c0 * c1, + c2: self.c1 * c1, + } + } + + pub fn mul_by_01(&self, c0: &Fp2, c1: &Fp2) -> Fp6 { + let a_a = self.c0 * c0; + let b_b = self.c1 * c1; + + let t1 = (self.c2 * c1).mul_by_nonresidue() + a_a; + + let t2 = (c0 + c1) * (self.c0 + self.c1) - a_a - b_b; + + let t3 = self.c2 * c0 + b_b; + + Fp6 { + c0: t1, + c1: t2, + c2: t3, + } + } + + /// Multiply by quadratic nonresidue v. + pub fn mul_by_nonresidue(&self) -> Self { + // Given a + bv + cv^2, this produces + // av + bv^2 + cv^3 + // but because v^3 = u + 1, we have + // c(u + 1) + av + v^2 + + Fp6 { + c0: self.c2.mul_by_nonresidue(), + c1: self.c0, + c2: self.c1, + } + } + + /// Raises this element to p. + #[inline(always)] + pub fn frobenius_map(&self) -> Self { + let c0 = self.c0.frobenius_map(); + let c1 = self.c1.frobenius_map(); + let c2 = self.c2.frobenius_map(); + + // c1 = c1 * (u + 1)^((p - 1) / 3) + let c1 = c1 + * Fp2 { + c0: Fp::zero(), + c1: Fp::from_raw_unchecked([ + 0xcd03_c9e4_8671_f071, + 0x5dab_2246_1fcd_a5d2, + 0x5870_42af_d385_1b95, + 0x8eb6_0ebe_01ba_cb9e, + 0x03f9_7d6e_83d0_50d2, + 0x18f0_2065_5463_8741, + ]), + }; + + // c2 = c2 * (u + 1)^((2p - 2) / 3) + let c2 = c2 + * Fp2 { + c0: Fp::from_raw_unchecked([ + 0x890d_c9e4_8675_45c3, + 0x2af3_2253_3285_a5d5, + 0x5088_0866_309b_7e2c, + 0xa20d_1b8c_7e88_1024, + 0x14e4_f04f_e2db_9068, + 0x14e5_6d3f_1564_853a, + ]), + c1: Fp::zero(), + }; + + Fp6 { c0, c1, c2 } + } + + #[inline(always)] + pub fn is_zero(&self) -> Choice { + self.c0.is_zero() & self.c1.is_zero() & self.c2.is_zero() + } + + /// Returns `c = self * b`. + /// + /// Implements the full-tower interleaving strategy from + /// [ePrint 2022-376](https://eprint.iacr.org/2022/367). + #[inline] + fn mul_interleaved(&self, b: &Self) -> Self { + // The intuition for this algorithm is that we can look at F_p^6 as a direct + // extension of F_p^2, and express the overall operations down to the base field + // F_p instead of only over F_p^2. This enables us to interleave multiplications + // and reductions, ensuring that we don't require double-width intermediate + // representations (with around twice as many limbs as F_p elements). + + // We want to express the multiplication c = a x b, where a = (a_0, a_1, a_2) is + // an element of F_p^6, and a_i = (a_i,0, a_i,1) is an element of F_p^2. The fully + // expanded multiplication is given by (2022-376 ยง5): + // + // c_0,0 = a_0,0 b_0,0 - a_0,1 b_0,1 + a_1,0 b_2,0 - a_1,1 b_2,1 + a_2,0 b_1,0 - a_2,1 b_1,1 + // - a_1,0 b_2,1 - a_1,1 b_2,0 - a_2,0 b_1,1 - a_2,1 b_1,0. + // = a_0,0 b_0,0 - a_0,1 b_0,1 + a_1,0 (b_2,0 - b_2,1) - a_1,1 (b_2,0 + b_2,1) + // + a_2,0 (b_1,0 - b_1,1) - a_2,1 (b_1,0 + b_1,1). + // + // c_0,1 = a_0,0 b_0,1 + a_0,1 b_0,0 + a_1,0 b_2,1 + a_1,1 b_2,0 + a_2,0 b_1,1 + a_2,1 b_1,0 + // + a_1,0 b_2,0 - a_1,1 b_2,1 + a_2,0 b_1,0 - a_2,1 b_1,1. + // = a_0,0 b_0,1 + a_0,1 b_0,0 + a_1,0(b_2,0 + b_2,1) + a_1,1(b_2,0 - b_2,1) + // + a_2,0(b_1,0 + b_1,1) + a_2,1(b_1,0 - b_1,1). + // + // c_1,0 = a_0,0 b_1,0 - a_0,1 b_1,1 + a_1,0 b_0,0 - a_1,1 b_0,1 + a_2,0 b_2,0 - a_2,1 b_2,1 + // - a_2,0 b_2,1 - a_2,1 b_2,0. + // = a_0,0 b_1,0 - a_0,1 b_1,1 + a_1,0 b_0,0 - a_1,1 b_0,1 + a_2,0(b_2,0 - b_2,1) + // - a_2,1(b_2,0 + b_2,1). + // + // c_1,1 = a_0,0 b_1,1 + a_0,1 b_1,0 + a_1,0 b_0,1 + a_1,1 b_0,0 + a_2,0 b_2,1 + a_2,1 b_2,0 + // + a_2,0 b_2,0 - a_2,1 b_2,1 + // = a_0,0 b_1,1 + a_0,1 b_1,0 + a_1,0 b_0,1 + a_1,1 b_0,0 + a_2,0(b_2,0 + b_2,1) + // + a_2,1(b_2,0 - b_2,1). + // + // c_2,0 = a_0,0 b_2,0 - a_0,1 b_2,1 + a_1,0 b_1,0 - a_1,1 b_1,1 + a_2,0 b_0,0 - a_2,1 b_0,1. + // c_2,1 = a_0,0 b_2,1 + a_0,1 b_2,0 + a_1,0 b_1,1 + a_1,1 b_1,0 + a_2,0 b_0,1 + a_2,1 b_0,0. + // + // Each of these is a "sum of products", which we can compute efficiently. + + let a = self; + let b10_p_b11 = b.c1.c0 + b.c1.c1; + let b10_m_b11 = b.c1.c0 - b.c1.c1; + let b20_p_b21 = b.c2.c0 + b.c2.c1; + let b20_m_b21 = b.c2.c0 - b.c2.c1; + + Fp6 { + c0: Fp2 { + c0: Fp::sum_of_products( + [a.c0.c0, -a.c0.c1, a.c1.c0, -a.c1.c1, a.c2.c0, -a.c2.c1], + [b.c0.c0, b.c0.c1, b20_m_b21, b20_p_b21, b10_m_b11, b10_p_b11], + ), + c1: Fp::sum_of_products( + [a.c0.c0, a.c0.c1, a.c1.c0, a.c1.c1, a.c2.c0, a.c2.c1], + [b.c0.c1, b.c0.c0, b20_p_b21, b20_m_b21, b10_p_b11, b10_m_b11], + ), + }, + c1: Fp2 { + c0: Fp::sum_of_products( + [a.c0.c0, -a.c0.c1, a.c1.c0, -a.c1.c1, a.c2.c0, -a.c2.c1], + [b.c1.c0, b.c1.c1, b.c0.c0, b.c0.c1, b20_m_b21, b20_p_b21], + ), + c1: Fp::sum_of_products( + [a.c0.c0, a.c0.c1, a.c1.c0, a.c1.c1, a.c2.c0, a.c2.c1], + [b.c1.c1, b.c1.c0, b.c0.c1, b.c0.c0, b20_p_b21, b20_m_b21], + ), + }, + c2: Fp2 { + c0: Fp::sum_of_products( + [a.c0.c0, -a.c0.c1, a.c1.c0, -a.c1.c1, a.c2.c0, -a.c2.c1], + [b.c2.c0, b.c2.c1, b.c1.c0, b.c1.c1, b.c0.c0, b.c0.c1], + ), + c1: Fp::sum_of_products( + [a.c0.c0, a.c0.c1, a.c1.c0, a.c1.c1, a.c2.c0, a.c2.c1], + [b.c2.c1, b.c2.c0, b.c1.c1, b.c1.c0, b.c0.c1, b.c0.c0], + ), + }, + } + } + + #[inline] + pub fn square(&self) -> Self { + let s0 = self.c0.square(); + let ab = self.c0 * self.c1; + let s1 = ab + ab; + let s2 = (self.c0 - self.c1 + self.c2).square(); + let bc = self.c1 * self.c2; + let s3 = bc + bc; + let s4 = self.c2.square(); + + Fp6 { + c0: s3.mul_by_nonresidue() + s0, + c1: s4.mul_by_nonresidue() + s1, + c2: s1 + s2 + s3 - s0 - s4, + } + } + + #[inline] + pub fn invert(&self) -> CtOption { + let c0 = (self.c1 * self.c2).mul_by_nonresidue(); + let c0 = self.c0.square() - c0; + + let c1 = self.c2.square().mul_by_nonresidue(); + let c1 = c1 - (self.c0 * self.c1); + + let c2 = self.c1.square(); + let c2 = c2 - (self.c0 * self.c2); + + let tmp = ((self.c1 * c2) + (self.c2 * c1)).mul_by_nonresidue(); + let tmp = tmp + (self.c0 * c0); + + tmp.invert().map(|t| Fp6 { + c0: t * c0, + c1: t * c1, + c2: t * c2, + }) + } +} + +impl<'a, 'b> Mul<&'b Fp6> for &'a Fp6 { + type Output = Fp6; + + #[inline] + fn mul(self, other: &'b Fp6) -> Self::Output { + self.mul_interleaved(other) + } +} + +impl<'a, 'b> Add<&'b Fp6> for &'a Fp6 { + type Output = Fp6; + + #[inline] + fn add(self, rhs: &'b Fp6) -> Self::Output { + Fp6 { + c0: self.c0 + rhs.c0, + c1: self.c1 + rhs.c1, + c2: self.c2 + rhs.c2, + } + } +} + +impl<'a> Neg for &'a Fp6 { + type Output = Fp6; + + #[inline] + fn neg(self) -> Self::Output { + Fp6 { + c0: -self.c0, + c1: -self.c1, + c2: -self.c2, + } + } +} + +impl Neg for Fp6 { + type Output = Fp6; + + #[inline] + fn neg(self) -> Self::Output { + -&self + } +} + +impl<'a, 'b> Sub<&'b Fp6> for &'a Fp6 { + type Output = Fp6; + + #[inline] + fn sub(self, rhs: &'b Fp6) -> Self::Output { + Fp6 { + c0: self.c0 - rhs.c0, + c1: self.c1 - rhs.c1, + c2: self.c2 - rhs.c2, + } + } +} + +impl_binops_additive!(Fp6, Fp6); +impl_binops_multiplicative!(Fp6, Fp6); + +#[test] +fn test_arithmetic() { + use crate::fp::*; + + let a = Fp6 { + c0: Fp2 { + c0: Fp::from_raw_unchecked([ + 0x47f9_cb98_b1b8_2d58, + 0x5fe9_11eb_a3aa_1d9d, + 0x96bf_1b5f_4dd8_1db3, + 0x8100_d27c_c925_9f5b, + 0xafa2_0b96_7464_0eab, + 0x09bb_cea7_d8d9_497d, + ]), + c1: Fp::from_raw_unchecked([ + 0x0303_cb98_b166_2daa, + 0xd931_10aa_0a62_1d5a, + 0xbfa9_820c_5be4_a468, + 0x0ba3_643e_cb05_a348, + 0xdc35_34bb_1f1c_25a6, + 0x06c3_05bb_19c0_e1c1, + ]), + }, + c1: Fp2 { + c0: Fp::from_raw_unchecked([ + 0x46f9_cb98_b162_d858, + 0x0be9_109c_f7aa_1d57, + 0xc791_bc55_fece_41d2, + 0xf84c_5770_4e38_5ec2, + 0xcb49_c1d9_c010_e60f, + 0x0acd_b8e1_58bf_e3c8, + ]), + c1: Fp::from_raw_unchecked([ + 0x8aef_cb98_b15f_8306, + 0x3ea1_108f_e4f2_1d54, + 0xcf79_f69f_a1b7_df3b, + 0xe4f5_4aa1_d16b_1a3c, + 0xba5e_4ef8_6105_a679, + 0x0ed8_6c07_97be_e5cf, + ]), + }, + c2: Fp2 { + c0: Fp::from_raw_unchecked([ + 0xcee5_cb98_b15c_2db4, + 0x7159_1082_d23a_1d51, + 0xd762_30e9_44a1_7ca4, + 0xd19e_3dd3_549d_d5b6, + 0xa972_dc17_01fa_66e3, + 0x12e3_1f2d_d6bd_e7d6, + ]), + c1: Fp::from_raw_unchecked([ + 0xad2a_cb98_b173_2d9d, + 0x2cfd_10dd_0696_1d64, + 0x0739_6b86_c6ef_24e8, + 0xbd76_e2fd_b1bf_c820, + 0x6afe_a7f6_de94_d0d5, + 0x1099_4b0c_5744_c040, + ]), + }, + }; + + let b = Fp6 { + c0: Fp2 { + c0: Fp::from_raw_unchecked([ + 0xf120_cb98_b16f_d84b, + 0x5fb5_10cf_f3de_1d61, + 0x0f21_a5d0_69d8_c251, + 0xaa1f_d62f_34f2_839a, + 0x5a13_3515_7f89_913f, + 0x14a3_fe32_9643_c247, + ]), + c1: Fp::from_raw_unchecked([ + 0x3516_cb98_b16c_82f9, + 0x926d_10c2_e126_1d5f, + 0x1709_e01a_0cc2_5fba, + 0x96c8_c960_b825_3f14, + 0x4927_c234_207e_51a9, + 0x18ae_b158_d542_c44e, + ]), + }, + c1: Fp2 { + c0: Fp::from_raw_unchecked([ + 0xbf0d_cb98_b169_82fc, + 0xa679_10b7_1d1a_1d5c, + 0xb7c1_47c2_b8fb_06ff, + 0x1efa_710d_47d2_e7ce, + 0xed20_a79c_7e27_653c, + 0x02b8_5294_dac1_dfba, + ]), + c1: Fp::from_raw_unchecked([ + 0x9d52_cb98_b180_82e5, + 0x621d_1111_5176_1d6f, + 0xe798_8260_3b48_af43, + 0x0ad3_1637_a4f4_da37, + 0xaeac_737c_5ac1_cf2e, + 0x006e_7e73_5b48_b824, + ]), + }, + c2: Fp2 { + c0: Fp::from_raw_unchecked([ + 0xe148_cb98_b17d_2d93, + 0x94d5_1104_3ebe_1d6c, + 0xef80_bca9_de32_4cac, + 0xf77c_0969_2827_95b1, + 0x9dc1_009a_fbb6_8f97, + 0x0479_3199_9a47_ba2b, + ]), + c1: Fp::from_raw_unchecked([ + 0x253e_cb98_b179_d841, + 0xc78d_10f7_2c06_1d6a, + 0xf768_f6f3_811b_ea15, + 0xe424_fc9a_ab5a_512b, + 0x8cd5_8db9_9cab_5001, + 0x0883_e4bf_d946_bc32, + ]), + }, + }; + + let c = Fp6 { + c0: Fp2 { + c0: Fp::from_raw_unchecked([ + 0x6934_cb98_b176_82ef, + 0xfa45_10ea_194e_1d67, + 0xff51_313d_2405_877e, + 0xd0cd_efcc_2e8d_0ca5, + 0x7bea_1ad8_3da0_106b, + 0x0c8e_97e6_1845_be39, + ]), + c1: Fp::from_raw_unchecked([ + 0x4779_cb98_b18d_82d8, + 0xb5e9_1144_4daa_1d7a, + 0x2f28_6bda_a653_2fc2, + 0xbca6_94f6_8bae_ff0f, + 0x3d75_e6b8_1a3a_7a5d, + 0x0a44_c3c4_98cc_96a3, + ]), + }, + c1: Fp2 { + c0: Fp::from_raw_unchecked([ + 0x8b6f_cb98_b18a_2d86, + 0xe8a1_1137_3af2_1d77, + 0x3710_a624_493c_cd2b, + 0xa94f_8828_0ee1_ba89, + 0x2c8a_73d6_bb2f_3ac7, + 0x0e4f_76ea_d7cb_98aa, + ]), + c1: Fp::from_raw_unchecked([ + 0xcf65_cb98_b186_d834, + 0x1b59_112a_283a_1d74, + 0x3ef8_e06d_ec26_6a95, + 0x95f8_7b59_9214_7603, + 0x1b9f_00f5_5c23_fb31, + 0x125a_2a11_16ca_9ab1, + ]), + }, + c2: Fp2 { + c0: Fp::from_raw_unchecked([ + 0x135b_cb98_b183_82e2, + 0x4e11_111d_1582_1d72, + 0x46e1_1ab7_8f10_07fe, + 0x82a1_6e8b_1547_317d, + 0x0ab3_8e13_fd18_bb9b, + 0x1664_dd37_55c9_9cb8, + ]), + c1: Fp::from_raw_unchecked([ + 0xce65_cb98_b131_8334, + 0xc759_0fdb_7c3a_1d2e, + 0x6fcb_8164_9d1c_8eb3, + 0x0d44_004d_1727_356a, + 0x3746_b738_a7d0_d296, + 0x136c_144a_96b1_34fc, + ]), + }, + }; + + assert_eq!(a.square(), a * a); + assert_eq!(b.square(), b * b); + assert_eq!(c.square(), c * c); + + assert_eq!((a + b) * c.square(), (c * c * a) + (c * c * b)); + + assert_eq!( + a.invert().unwrap() * b.invert().unwrap(), + (a * b).invert().unwrap() + ); + assert_eq!(a.invert().unwrap() * a, Fp6::one()); +} + +#[cfg(feature = "zeroize")] +#[test] +fn test_zeroize() { + use zeroize::Zeroize; + + let mut a = Fp6::one(); + a.zeroize(); + assert!(bool::from(a.is_zero())); +} diff --git a/constantine/bls12_381/src/g1.rs b/constantine/bls12_381/src/g1.rs new file mode 100644 index 000000000..bf4676659 --- /dev/null +++ b/constantine/bls12_381/src/g1.rs @@ -0,0 +1,1798 @@ +//! This module provides an implementation of the $\mathbb{G}_1$ group of BLS12-381. + +use core::borrow::Borrow; +use core::fmt; +use core::iter::Sum; +use core::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}; +use group::{ + prime::{PrimeCurve, PrimeCurveAffine, PrimeGroup}, + Curve, Group, GroupEncoding, UncompressedEncoding, +}; +use rand_core::RngCore; +use subtle::{Choice, ConditionallySelectable, ConstantTimeEq, CtOption}; + +#[cfg(feature = "alloc")] +use group::WnafGroup; + +use crate::fp::Fp; +use crate::Scalar; + +/// 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. +/// +/// Values of `G1Affine` are guaranteed to be in the $q$-order subgroup unless an +/// "unchecked" API was misused. +#[cfg_attr(docsrs, doc(cfg(feature = "groups")))] +#[derive(Copy, Clone, Debug)] +pub struct G1Affine { + pub(crate) x: Fp, + pub(crate) y: Fp, + infinity: Choice, +} + +impl Default for G1Affine { + fn default() -> G1Affine { + G1Affine::identity() + } +} + +#[cfg(feature = "zeroize")] +impl zeroize::DefaultIsZeroes for G1Affine {} + +impl fmt::Display for G1Affine { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +impl<'a> From<&'a G1Projective> for G1Affine { + fn from(p: &'a G1Projective) -> G1Affine { + let zinv = p.z.invert().unwrap_or(Fp::zero()); + let x = p.x * zinv; + let y = p.y * zinv; + + let tmp = G1Affine { + x, + y, + infinity: Choice::from(0u8), + }; + + G1Affine::conditional_select(&tmp, &G1Affine::identity(), zinv.is_zero()) + } +} + +impl From for G1Affine { + fn from(p: G1Projective) -> G1Affine { + G1Affine::from(&p) + } +} + +impl ConstantTimeEq for G1Affine { + fn ct_eq(&self, other: &Self) -> Choice { + // The only cases in which two points are equal are + // 1. infinity is set on both + // 2. infinity is not set on both, and their coordinates are equal + + (self.infinity & other.infinity) + | ((!self.infinity) + & (!other.infinity) + & self.x.ct_eq(&other.x) + & self.y.ct_eq(&other.y)) + } +} + +impl ConditionallySelectable for G1Affine { + fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self { + G1Affine { + x: Fp::conditional_select(&a.x, &b.x, choice), + y: Fp::conditional_select(&a.y, &b.y, choice), + infinity: Choice::conditional_select(&a.infinity, &b.infinity, choice), + } + } +} + +impl Eq for G1Affine {} +impl PartialEq for G1Affine { + #[inline] + fn eq(&self, other: &Self) -> bool { + bool::from(self.ct_eq(other)) + } +} + +impl<'a> Neg for &'a G1Affine { + type Output = G1Affine; + + #[inline] + fn neg(self) -> G1Affine { + G1Affine { + x: self.x, + y: Fp::conditional_select(&-self.y, &Fp::one(), self.infinity), + infinity: self.infinity, + } + } +} + +impl Neg for G1Affine { + type Output = G1Affine; + + #[inline] + fn neg(self) -> G1Affine { + -&self + } +} + +impl<'a, 'b> Add<&'b G1Projective> for &'a G1Affine { + type Output = G1Projective; + + #[inline] + fn add(self, rhs: &'b G1Projective) -> G1Projective { + rhs.add_mixed(self) + } +} + +impl<'a, 'b> Add<&'b G1Affine> for &'a G1Projective { + type Output = G1Projective; + + #[inline] + fn add(self, rhs: &'b G1Affine) -> G1Projective { + self.add_mixed(rhs) + } +} + +impl<'a, 'b> Sub<&'b G1Projective> for &'a G1Affine { + type Output = G1Projective; + + #[inline] + fn sub(self, rhs: &'b G1Projective) -> G1Projective { + self + (-rhs) + } +} + +impl<'a, 'b> Sub<&'b G1Affine> for &'a G1Projective { + type Output = G1Projective; + + #[inline] + fn sub(self, rhs: &'b G1Affine) -> G1Projective { + self + (-rhs) + } +} + +impl Sum for G1Projective +where + T: Borrow, +{ + fn sum(iter: I) -> Self + where + I: Iterator, + { + iter.fold(Self::identity(), |acc, item| acc + item.borrow()) + } +} + +impl_binops_additive!(G1Projective, G1Affine); +impl_binops_additive_specify_output!(G1Affine, G1Projective, G1Projective); + +const B: Fp = Fp::from_raw_unchecked([ + 0xaa27_0000_000c_fff3, + 0x53cc_0032_fc34_000a, + 0x478f_e97a_6b0a_807f, + 0xb1d3_7ebe_e6ba_24d7, + 0x8ec9_733b_bf78_ab2f, + 0x09d6_4551_3d83_de7e, +]); + +impl G1Affine { + /// Returns the identity of the group: the point at infinity. + pub fn identity() -> G1Affine { + G1Affine { + x: Fp::zero(), + y: Fp::one(), + infinity: Choice::from(1u8), + } + } + + /// Returns a fixed generator of the group. See [`notes::design`](notes/design/index.html#fixed-generators) + /// for how this generator is chosen. + pub fn generator() -> G1Affine { + G1Affine { + x: Fp::from_raw_unchecked([ + 0x5cb3_8790_fd53_0c16, + 0x7817_fc67_9976_fff5, + 0x154f_95c7_143b_a1c1, + 0xf0ae_6acd_f3d0_e747, + 0xedce_6ecc_21db_f440, + 0x1201_7741_9e0b_fb75, + ]), + y: Fp::from_raw_unchecked([ + 0xbaac_93d5_0ce7_2271, + 0x8c22_631a_7918_fd8e, + 0xdd59_5f13_5707_25ce, + 0x51ac_5829_5040_5194, + 0x0e1c_8c3f_ad00_59c0, + 0x0bbc_3efc_5008_a26a, + ]), + infinity: Choice::from(0u8), + } + } + + /// Serializes this element into compressed form. See [`notes::serialization`](crate::notes::serialization) + /// for details about how group elements are serialized. + pub fn to_compressed(&self) -> [u8; 48] { + // Strictly speaking, self.x is zero already when self.infinity is true, but + // to guard against implementation mistakes we do not assume this. + let mut res = Fp::conditional_select(&self.x, &Fp::zero(), self.infinity).to_bytes(); + + // This point is in compressed form, so we set the most significant bit. + res[0] |= 1u8 << 7; + + // Is this point at infinity? If so, set the second-most significant bit. + res[0] |= u8::conditional_select(&0u8, &(1u8 << 6), self.infinity); + + // Is the y-coordinate the lexicographically largest of the two associated with the + // x-coordinate? If so, set the third-most significant bit so long as this is not + // the point at infinity. + res[0] |= u8::conditional_select( + &0u8, + &(1u8 << 5), + (!self.infinity) & self.y.lexicographically_largest(), + ); + + res + } + + /// Serializes this element into uncompressed form. See [`notes::serialization`](crate::notes::serialization) + /// for details about how group elements are serialized. + pub fn to_uncompressed(&self) -> [u8; 96] { + let mut res = [0; 96]; + + res[0..48].copy_from_slice( + &Fp::conditional_select(&self.x, &Fp::zero(), self.infinity).to_bytes()[..], + ); + res[48..96].copy_from_slice( + &Fp::conditional_select(&self.y, &Fp::zero(), self.infinity).to_bytes()[..], + ); + + // Is this point at infinity? If so, set the second-most significant bit. + res[0] |= u8::conditional_select(&0u8, &(1u8 << 6), self.infinity); + + res + } + + /// Attempts to deserialize an uncompressed element. See [`notes::serialization`](crate::notes::serialization) + /// for details about how group elements are serialized. + pub fn from_uncompressed(bytes: &[u8; 96]) -> CtOption { + Self::from_uncompressed_unchecked(bytes) + .and_then(|p| CtOption::new(p, p.is_on_curve() & p.is_torsion_free())) + } + + /// Attempts to deserialize an uncompressed element, not checking if the + /// element is on the curve and not checking if it is in the correct subgroup. + /// **This is dangerous to call unless you trust the bytes you are reading; otherwise, + /// API invariants may be broken.** Please consider using `from_uncompressed()` instead. + pub fn from_uncompressed_unchecked(bytes: &[u8; 96]) -> CtOption { + // Obtain the three flags from the start of the byte sequence + let compression_flag_set = Choice::from((bytes[0] >> 7) & 1); + let infinity_flag_set = Choice::from((bytes[0] >> 6) & 1); + let sort_flag_set = Choice::from((bytes[0] >> 5) & 1); + + // Attempt to obtain the x-coordinate + let x = { + let mut tmp = [0; 48]; + tmp.copy_from_slice(&bytes[0..48]); + + // Mask away the flag bits + tmp[0] &= 0b0001_1111; + + Fp::from_bytes(&tmp) + }; + + // Attempt to obtain the y-coordinate + let y = { + let mut tmp = [0; 48]; + tmp.copy_from_slice(&bytes[48..96]); + + Fp::from_bytes(&tmp) + }; + + x.and_then(|x| { + y.and_then(|y| { + // Create a point representing this value + let p = G1Affine::conditional_select( + &G1Affine { + x, + y, + infinity: infinity_flag_set, + }, + &G1Affine::identity(), + infinity_flag_set, + ); + + CtOption::new( + p, + // If the infinity flag is set, the x and y coordinates should have been zero. + ((!infinity_flag_set) | (infinity_flag_set & x.is_zero() & y.is_zero())) & + // The compression flag should not have been set, as this is an uncompressed element + (!compression_flag_set) & + // The sort flag should not have been set, as this is an uncompressed element + (!sort_flag_set), + ) + }) + }) + } + + /// Attempts to deserialize a compressed element. See [`notes::serialization`](crate::notes::serialization) + /// for details about how group elements are serialized. + pub fn from_compressed(bytes: &[u8; 48]) -> CtOption { + // We already know the point is on the curve because this is established + // by the y-coordinate recovery procedure in from_compressed_unchecked(). + + Self::from_compressed_unchecked(bytes).and_then(|p| CtOption::new(p, p.is_torsion_free())) + } + + /// Attempts to deserialize an uncompressed element, not checking if the + /// element is in the correct subgroup. + /// **This is dangerous to call unless you trust the bytes you are reading; otherwise, + /// API invariants may be broken.** Please consider using `from_compressed()` instead. + pub fn from_compressed_unchecked(bytes: &[u8; 48]) -> CtOption { + // Obtain the three flags from the start of the byte sequence + let compression_flag_set = Choice::from((bytes[0] >> 7) & 1); + let infinity_flag_set = Choice::from((bytes[0] >> 6) & 1); + let sort_flag_set = Choice::from((bytes[0] >> 5) & 1); + + // Attempt to obtain the x-coordinate + let x = { + let mut tmp = [0; 48]; + tmp.copy_from_slice(&bytes[0..48]); + + // Mask away the flag bits + tmp[0] &= 0b0001_1111; + + Fp::from_bytes(&tmp) + }; + + x.and_then(|x| { + // If the infinity flag is set, return the value assuming + // the x-coordinate is zero and the sort bit is not set. + // + // Otherwise, return a recovered point (assuming the correct + // y-coordinate can be found) so long as the infinity flag + // was not set. + CtOption::new( + G1Affine::identity(), + infinity_flag_set & // Infinity flag should be set + compression_flag_set & // Compression flag should be set + (!sort_flag_set) & // Sort flag should not be set + x.is_zero(), // The x-coordinate should be zero + ) + .or_else(|| { + // Recover a y-coordinate given x by y = sqrt(x^3 + 4) + ((x.square() * x) + B).sqrt().and_then(|y| { + // Switch to the correct y-coordinate if necessary. + let y = Fp::conditional_select( + &y, + &-y, + y.lexicographically_largest() ^ sort_flag_set, + ); + + CtOption::new( + G1Affine { + x, + y, + infinity: infinity_flag_set, + }, + (!infinity_flag_set) & // Infinity flag should not be set + compression_flag_set, // Compression flag should be set + ) + }) + }) + }) + } + + /// Returns true if this element is the identity (the point at infinity). + #[inline] + pub fn is_identity(&self) -> Choice { + self.infinity + } + + /// Returns true if this point is free of an $h$-torsion component, and so it + /// exists within the $q$-order subgroup $\mathbb{G}_1$. This should always return true + /// unless an "unchecked" API was used. + pub fn is_torsion_free(&self) -> Choice { + // Algorithm from Section 6 of https://eprint.iacr.org/2021/1130 + // Updated proof of correctness in https://eprint.iacr.org/2022/352 + // + // Check that endomorphism_p(P) == -[x^2] P + + let minus_x_squared_times_p = G1Projective::from(self).mul_by_x().mul_by_x().neg(); + let endomorphism_p = endomorphism(self); + minus_x_squared_times_p.ct_eq(&G1Projective::from(endomorphism_p)) + } + + /// Returns true if this point is on the curve. This should always return + /// true unless an "unchecked" API was used. + pub fn is_on_curve(&self) -> Choice { + // y^2 - x^3 ?= 4 + (self.y.square() - (self.x.square() * self.x)).ct_eq(&B) | self.infinity + } +} + +/// A nontrivial third root of unity in Fp +pub const BETA: Fp = Fp::from_raw_unchecked([ + 0x30f1_361b_798a_64e8, + 0xf3b8_ddab_7ece_5a2a, + 0x16a8_ca3a_c615_77f7, + 0xc26a_2ff8_74fd_029b, + 0x3636_b766_6070_1c6e, + 0x051b_a4ab_241b_6160, +]); + +fn endomorphism(p: &G1Affine) -> G1Affine { + // Endomorphism of the points on the curve. + // endomorphism_p(x,y) = (BETA * x, y) + // where BETA is a non-trivial cubic root of unity in Fq. + let mut res = *p; + res.x *= BETA; + res +} + +/// This is an element of $\mathbb{G}_1$ represented in the projective coordinate space. +#[cfg_attr(docsrs, doc(cfg(feature = "groups")))] +#[derive(Copy, Clone, Debug)] +pub struct G1Projective { + pub x: Fp, + pub y: Fp, + pub z: Fp, +} + +impl Default for G1Projective { + fn default() -> G1Projective { + G1Projective::identity() + } +} + +#[cfg(feature = "zeroize")] +impl zeroize::DefaultIsZeroes for G1Projective {} + +impl fmt::Display for G1Projective { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +impl<'a> From<&'a G1Affine> for G1Projective { + fn from(p: &'a G1Affine) -> G1Projective { + G1Projective { + x: p.x, + y: p.y, + z: Fp::conditional_select(&Fp::one(), &Fp::zero(), p.infinity), + } + } +} + +impl From for G1Projective { + fn from(p: G1Affine) -> G1Projective { + G1Projective::from(&p) + } +} + +impl ConstantTimeEq for G1Projective { + fn ct_eq(&self, other: &Self) -> Choice { + // Is (xz, yz, z) equal to (x'z', y'z', z') when converted to affine? + + let x1 = self.x * other.z; + let x2 = other.x * self.z; + + let y1 = self.y * other.z; + let y2 = other.y * self.z; + + let self_is_zero = self.z.is_zero(); + let other_is_zero = other.z.is_zero(); + + (self_is_zero & other_is_zero) // Both point at infinity + | ((!self_is_zero) & (!other_is_zero) & x1.ct_eq(&x2) & y1.ct_eq(&y2)) + // Neither point at infinity, coordinates are the same + } +} + +impl ConditionallySelectable for G1Projective { + fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self { + G1Projective { + x: Fp::conditional_select(&a.x, &b.x, choice), + y: Fp::conditional_select(&a.y, &b.y, choice), + z: Fp::conditional_select(&a.z, &b.z, choice), + } + } +} + +impl Eq for G1Projective {} +impl PartialEq for G1Projective { + #[inline] + fn eq(&self, other: &Self) -> bool { + bool::from(self.ct_eq(other)) + } +} + +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<'a, 'b> Mul<&'b Scalar> for &'a G1Projective { + type Output = G1Projective; + + fn mul(self, other: &'b Scalar) -> Self::Output { + self.multiply(&other.to_bytes()) + } +} + +impl<'a, 'b> Mul<&'b G1Projective> for &'a Scalar { + type Output = G1Projective; + + #[inline] + fn mul(self, rhs: &'b G1Projective) -> Self::Output { + rhs * self + } +} + +impl<'a, 'b> Mul<&'b Scalar> for &'a G1Affine { + type Output = G1Projective; + + fn mul(self, other: &'b Scalar) -> Self::Output { + G1Projective::from(self).multiply(&other.to_bytes()) + } +} + +impl<'a, 'b> Mul<&'b G1Affine> for &'a Scalar { + type Output = G1Projective; + + #[inline] + fn mul(self, rhs: &'b G1Affine) -> Self::Output { + rhs * self + } +} + +impl_binops_additive!(G1Projective, G1Projective); +impl_binops_multiplicative!(G1Projective, Scalar); +impl_binops_multiplicative_mixed!(G1Affine, Scalar, G1Projective); +impl_binops_multiplicative_mixed!(Scalar, G1Affine, G1Projective); +impl_binops_multiplicative_mixed!(Scalar, G1Projective, G1Projective); + +#[inline(always)] +fn mul_by_3b(a: Fp) -> Fp { + let a = a + a; // 2 + let a = a + a; // 4 + a + a + a // 12 +} + +impl G1Projective { + /// Returns the identity of the group: the point at infinity. + /// + pub fn identity() -> G1Projective { + G1Projective { + x: Fp::zero(), + y: Fp::one(), + z: Fp::zero(), + } + } + + /// Returns a fixed generator of the group. See [`notes::design`](notes/design/index.html#fixed-generators) + /// for how this generator is chosen. + pub fn generator() -> G1Projective { + G1Projective { + x: Fp::from_raw_unchecked([ + 0x5cb3_8790_fd53_0c16, + 0x7817_fc67_9976_fff5, + 0x154f_95c7_143b_a1c1, + 0xf0ae_6acd_f3d0_e747, + 0xedce_6ecc_21db_f440, + 0x1201_7741_9e0b_fb75, + ]), + y: Fp::from_raw_unchecked([ + 0xbaac_93d5_0ce7_2271, + 0x8c22_631a_7918_fd8e, + 0xdd59_5f13_5707_25ce, + 0x51ac_5829_5040_5194, + 0x0e1c_8c3f_ad00_59c0, + 0x0bbc_3efc_5008_a26a, + ]), + z: Fp::one(), + } + } + + /// Computes the doubling of this point. + pub fn double(&self) -> G1Projective { + // Algorithm 9, https://eprint.iacr.org/2015/1060.pdf + + let t0 = self.y.square(); + let z3 = t0 + t0; + let z3 = z3 + z3; + let z3 = z3 + z3; + let t1 = self.y * self.z; + let t2 = self.z.square(); + let t2 = mul_by_3b(t2); + let x3 = t2 * z3; + let y3 = t0 + t2; + let z3 = t1 * z3; + let t1 = t2 + t2; + let t2 = t1 + t2; + let t0 = t0 - t2; + let y3 = t0 * y3; + let y3 = x3 + y3; + let t1 = self.x * self.y; + let x3 = t0 * t1; + let x3 = x3 + x3; + + let tmp = G1Projective { + x: x3, + y: y3, + z: z3, + }; + + G1Projective::conditional_select(&tmp, &G1Projective::identity(), self.is_identity()) + } + + /// Adds this point to another point. + pub fn add(&self, rhs: &G1Projective) -> G1Projective { + // Algorithm 7, https://eprint.iacr.org/2015/1060.pdf + + let t0 = self.x * rhs.x; + let t1 = self.y * rhs.y; + let t2 = self.z * rhs.z; + let t3 = self.x + self.y; + let t4 = rhs.x + rhs.y; + let t3 = t3 * t4; + let t4 = t0 + t1; + let t3 = t3 - t4; + let t4 = self.y + self.z; + let x3 = rhs.y + rhs.z; + let t4 = t4 * x3; + let x3 = t1 + t2; + let t4 = t4 - x3; + let x3 = self.x + self.z; + let y3 = rhs.x + rhs.z; + let x3 = x3 * y3; + let y3 = t0 + t2; + let y3 = x3 - y3; + let x3 = t0 + t0; + let t0 = x3 + t0; + let t2 = mul_by_3b(t2); + let z3 = t1 + t2; + let t1 = t1 - t2; + let y3 = mul_by_3b(y3); + let x3 = t4 * y3; + let t2 = t3 * t1; + let x3 = t2 - x3; + let y3 = y3 * t0; + let t1 = t1 * z3; + let y3 = t1 + y3; + let t0 = t0 * t3; + let z3 = z3 * t4; + let z3 = z3 + t0; + + G1Projective { + x: x3, + y: y3, + z: z3, + } + } + + /// Adds this point to another point in the affine model. + pub fn add_mixed(&self, rhs: &G1Affine) -> G1Projective { + // Algorithm 8, https://eprint.iacr.org/2015/1060.pdf + + let t0 = self.x * rhs.x; + let t1 = self.y * rhs.y; + let t3 = rhs.x + rhs.y; + let t4 = self.x + self.y; + let t3 = t3 * t4; + let t4 = t0 + t1; + let t3 = t3 - t4; + let t4 = rhs.y * self.z; + let t4 = t4 + self.y; + let y3 = rhs.x * self.z; + let y3 = y3 + self.x; + let x3 = t0 + t0; + let t0 = x3 + t0; + let t2 = mul_by_3b(self.z); + let z3 = t1 + t2; + let t1 = t1 - t2; + let y3 = mul_by_3b(y3); + let x3 = t4 * y3; + let t2 = t3 * t1; + let x3 = t2 - x3; + let y3 = y3 * t0; + let t1 = t1 * z3; + let y3 = t1 + y3; + let t0 = t0 * t3; + let z3 = z3 * t4; + let z3 = z3 + t0; + + let tmp = G1Projective { + x: x3, + y: y3, + z: z3, + }; + + G1Projective::conditional_select(&tmp, self, rhs.is_identity()) + } + + pub fn random(mut rng: impl RngCore) -> Self { + loop { + let x = Fp::random(&mut rng); + let flip_sign = rng.next_u32() % 2 != 0; + + // Obtain the corresponding y-coordinate given x as y = sqrt(x^3 + 4) + let p = ((x.square() * x) + B).sqrt().map(|y| G1Affine { + x, + y: if flip_sign { -y } else { y }, + infinity: 0.into(), + }); + + if p.is_some().into() { + let p = p.unwrap().to_curve().clear_cofactor(); + + if bool::from(!p.is_identity()) { + return p; + } + } + } + } + fn multiply(&self, by: &[u8; 32]) -> G1Projective { + let mut acc = G1Projective::identity(); + + // This is a simple double-and-add implementation of point + // multiplication, moving from most significant to least + // significant bit of the scalar. + // + // We skip the leading bit because it's always unset for Fq + // elements. + for bit in by + .iter() + .rev() + .flat_map(|byte| (0..8).rev().map(move |i| Choice::from((byte >> i) & 1u8))) + .skip(1) + { + acc = acc.double(); + acc = G1Projective::conditional_select(&acc, &(acc + self), bit); + } + + acc + } + + /// Multiply `self` by `crate::BLS_X`, using double and add. + fn mul_by_x(&self) -> G1Projective { + let mut xself = G1Projective::identity(); + // NOTE: in BLS12-381 we can just skip the first bit. + let mut x = crate::BLS_X >> 1; + let mut tmp = *self; + while x != 0 { + tmp = tmp.double(); + + if x % 2 == 1 { + xself += tmp; + } + x >>= 1; + } + // finally, flip the sign + if crate::BLS_X_IS_NEGATIVE { + xself = -xself; + } + xself + } + + /// Multiplies by $(1 - z)$, where $z$ is the parameter of BLS12-381, which + /// [suffices to clear](https://ia.cr/2019/403) the cofactor and map + /// elliptic curve points to elements of $\mathbb{G}\_1$. + pub fn clear_cofactor(&self) -> G1Projective { + self - self.mul_by_x() + } + + /// Converts a batch of `G1Projective` elements into `G1Affine` elements. This + /// function will panic if `p.len() != q.len()`. + pub fn batch_normalize(p: &[Self], q: &mut [G1Affine]) { + assert_eq!(p.len(), q.len()); + + let mut acc = Fp::one(); + for (p, q) in p.iter().zip(q.iter_mut()) { + // We use the `x` field of `G1Affine` to store the product + // of previous z-coordinates seen. + q.x = acc; + + // We will end up skipping all identities in p + acc = Fp::conditional_select(&(acc * p.z), &acc, p.is_identity()); + } + + // This is the inverse, as all z-coordinates are nonzero and the ones + // that are not are skipped. + acc = acc.invert().unwrap(); + + for (p, q) in p.iter().rev().zip(q.iter_mut().rev()) { + let skip = p.is_identity(); + + // Compute tmp = 1/z + let tmp = q.x * acc; + + // Cancel out z-coordinate in denominator of `acc` + acc = Fp::conditional_select(&(acc * p.z), &acc, skip); + + // Set the coordinates to the correct value + q.x = p.x * tmp; + q.y = p.y * tmp; + q.infinity = Choice::from(0u8); + + *q = G1Affine::conditional_select(q, &G1Affine::identity(), skip); + } + } + + /// Returns true if this element is the identity (the point at infinity). + #[inline] + pub fn is_identity(&self) -> Choice { + self.z.is_zero() + } + + /// Returns true if this point is on the curve. This should always return + /// true unless an "unchecked" API was used. + pub fn is_on_curve(&self) -> Choice { + // Y^2 Z = X^3 + b Z^3 + + (self.y.square() * self.z).ct_eq(&(self.x.square() * self.x + self.z.square() * self.z * B)) + | self.z.is_zero() + } +} + +#[derive(Clone, Copy)] +pub struct G1Compressed([u8; 48]); + +impl fmt::Debug for G1Compressed { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0[..].fmt(f) + } +} + +impl Default for G1Compressed { + fn default() -> Self { + G1Compressed([0; 48]) + } +} + +#[cfg(feature = "zeroize")] +impl zeroize::DefaultIsZeroes for G1Compressed {} + +impl AsRef<[u8]> for G1Compressed { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl AsMut<[u8]> for G1Compressed { + fn as_mut(&mut self) -> &mut [u8] { + &mut self.0 + } +} + +impl ConstantTimeEq for G1Compressed { + fn ct_eq(&self, other: &Self) -> Choice { + self.0.ct_eq(&other.0) + } +} + +impl Eq for G1Compressed {} +impl PartialEq for G1Compressed { + #[inline] + fn eq(&self, other: &Self) -> bool { + bool::from(self.ct_eq(other)) + } +} + +#[derive(Clone, Copy)] +pub struct G1Uncompressed([u8; 96]); + +impl fmt::Debug for G1Uncompressed { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0[..].fmt(f) + } +} + +impl Default for G1Uncompressed { + fn default() -> Self { + G1Uncompressed([0; 96]) + } +} + +#[cfg(feature = "zeroize")] +impl zeroize::DefaultIsZeroes for G1Uncompressed {} + +impl AsRef<[u8]> for G1Uncompressed { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl AsMut<[u8]> for G1Uncompressed { + fn as_mut(&mut self) -> &mut [u8] { + &mut self.0 + } +} + +impl ConstantTimeEq for G1Uncompressed { + fn ct_eq(&self, other: &Self) -> Choice { + self.0.ct_eq(&other.0) + } +} + +impl Eq for G1Uncompressed {} +impl PartialEq for G1Uncompressed { + #[inline] + fn eq(&self, other: &Self) -> bool { + bool::from(self.ct_eq(other)) + } +} + +impl Group for G1Projective { + type Scalar = Scalar; + + fn random(mut rng: impl RngCore) -> Self { + loop { + let x = Fp::random(&mut rng); + let flip_sign = rng.next_u32() % 2 != 0; + + // Obtain the corresponding y-coordinate given x as y = sqrt(x^3 + 4) + let p = ((x.square() * x) + B).sqrt().map(|y| G1Affine { + x, + y: if flip_sign { -y } else { y }, + infinity: 0.into(), + }); + + if p.is_some().into() { + let p = p.unwrap().to_curve().clear_cofactor(); + + if bool::from(!p.is_identity()) { + return p; + } + } + } + } + + fn identity() -> Self { + Self::identity() + } + + fn generator() -> Self { + Self::generator() + } + + fn is_identity(&self) -> Choice { + self.is_identity() + } + + #[must_use] + fn double(&self) -> Self { + self.double() + } +} + +#[cfg(feature = "alloc")] +impl WnafGroup for G1Projective { + fn recommended_wnaf_for_num_scalars(num_scalars: usize) -> usize { + const RECOMMENDATIONS: [usize; 12] = + [1, 3, 7, 20, 43, 120, 273, 563, 1630, 3128, 7933, 62569]; + + let mut ret = 4; + for r in &RECOMMENDATIONS { + if num_scalars > *r { + ret += 1; + } else { + break; + } + } + + ret + } +} + +impl PrimeGroup for G1Projective {} + +impl Curve for G1Projective { + type AffineRepr = G1Affine; + + fn batch_normalize(p: &[Self], q: &mut [Self::AffineRepr]) { + Self::batch_normalize(p, q); + } + + fn to_affine(&self) -> Self::AffineRepr { + self.into() + } +} + +impl PrimeCurve for G1Projective { + type Affine = G1Affine; +} + +impl PrimeCurveAffine for G1Affine { + type Scalar = Scalar; + type Curve = G1Projective; + + fn identity() -> Self { + Self::identity() + } + + fn generator() -> Self { + Self::generator() + } + + fn is_identity(&self) -> Choice { + self.is_identity() + } + + fn to_curve(&self) -> Self::Curve { + self.into() + } +} + +impl GroupEncoding for G1Projective { + type Repr = G1Compressed; + + fn from_bytes(bytes: &Self::Repr) -> CtOption { + G1Affine::from_bytes(bytes).map(Self::from) + } + + fn from_bytes_unchecked(bytes: &Self::Repr) -> CtOption { + G1Affine::from_bytes_unchecked(bytes).map(Self::from) + } + + fn to_bytes(&self) -> Self::Repr { + G1Affine::from(self).to_bytes() + } +} + +impl GroupEncoding for G1Affine { + type Repr = G1Compressed; + + fn from_bytes(bytes: &Self::Repr) -> CtOption { + Self::from_compressed(&bytes.0) + } + + fn from_bytes_unchecked(bytes: &Self::Repr) -> CtOption { + Self::from_compressed_unchecked(&bytes.0) + } + + fn to_bytes(&self) -> Self::Repr { + G1Compressed(self.to_compressed()) + } +} + +impl UncompressedEncoding for G1Affine { + type Uncompressed = G1Uncompressed; + + fn from_uncompressed(bytes: &Self::Uncompressed) -> CtOption { + Self::from_uncompressed(&bytes.0) + } + + fn from_uncompressed_unchecked(bytes: &Self::Uncompressed) -> CtOption { + Self::from_uncompressed_unchecked(&bytes.0) + } + + fn to_uncompressed(&self) -> Self::Uncompressed { + G1Uncompressed(self.to_uncompressed()) + } +} + +#[test] +fn test_beta() { + assert_eq!( + BETA, + Fp::from_bytes(&[ + 0x00u8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5f, 0x19, 0x67, 0x2f, 0xdf, 0x76, + 0xce, 0x51, 0xba, 0x69, 0xc6, 0x07, 0x6a, 0x0f, 0x77, 0xea, 0xdd, 0xb3, 0xa9, 0x3b, + 0xe6, 0xf8, 0x96, 0x88, 0xde, 0x17, 0xd8, 0x13, 0x62, 0x0a, 0x00, 0x02, 0x2e, 0x01, + 0xff, 0xff, 0xff, 0xfe, 0xff, 0xfe + ]) + .unwrap() + ); + assert_ne!(BETA, Fp::one()); + assert_ne!(BETA * BETA, Fp::one()); + assert_eq!(BETA * BETA * BETA, Fp::one()); +} +#[test] +fn test_is_on_curve() { + assert!(bool::from(G1Affine::identity().is_on_curve())); + assert!(bool::from(G1Affine::generator().is_on_curve())); + assert!(bool::from(G1Projective::identity().is_on_curve())); + assert!(bool::from(G1Projective::generator().is_on_curve())); + + let z = Fp::from_raw_unchecked([ + 0xba7a_fa1f_9a6f_e250, + 0xfa0f_5b59_5eaf_e731, + 0x3bdc_4776_94c3_06e7, + 0x2149_be4b_3949_fa24, + 0x64aa_6e06_49b2_078c, + 0x12b1_08ac_3364_3c3e, + ]); + + let gen = G1Affine::generator(); + let mut test = G1Projective { + x: gen.x * z, + y: gen.y * z, + z, + }; + + assert!(bool::from(test.is_on_curve())); + + test.x = z; + assert!(!bool::from(test.is_on_curve())); +} + +#[test] +#[allow(clippy::eq_op)] +fn test_affine_point_equality() { + let a = G1Affine::generator(); + let b = G1Affine::identity(); + + assert!(a == a); + assert!(b == b); + assert!(a != b); + assert!(b != a); +} + +#[test] +#[allow(clippy::eq_op)] +fn test_projective_point_equality() { + let a = G1Projective::generator(); + let b = G1Projective::identity(); + + assert!(a == a); + assert!(b == b); + assert!(a != b); + assert!(b != a); + + let z = Fp::from_raw_unchecked([ + 0xba7a_fa1f_9a6f_e250, + 0xfa0f_5b59_5eaf_e731, + 0x3bdc_4776_94c3_06e7, + 0x2149_be4b_3949_fa24, + 0x64aa_6e06_49b2_078c, + 0x12b1_08ac_3364_3c3e, + ]); + + let mut c = G1Projective { + x: a.x * z, + y: a.y * z, + z, + }; + assert!(bool::from(c.is_on_curve())); + + assert!(a == c); + assert!(b != c); + assert!(c == a); + assert!(c != b); + + c.y = -c.y; + assert!(bool::from(c.is_on_curve())); + + assert!(a != c); + assert!(b != c); + assert!(c != a); + assert!(c != b); + + c.y = -c.y; + c.x = z; + assert!(!bool::from(c.is_on_curve())); + assert!(a != b); + assert!(a != c); + assert!(b != c); +} + +#[test] +fn test_conditionally_select_affine() { + let a = G1Affine::generator(); + let b = G1Affine::identity(); + + assert_eq!(G1Affine::conditional_select(&a, &b, Choice::from(0u8)), a); + assert_eq!(G1Affine::conditional_select(&a, &b, Choice::from(1u8)), b); +} + +#[test] +fn test_conditionally_select_projective() { + let a = G1Projective::generator(); + let b = G1Projective::identity(); + + assert_eq!( + G1Projective::conditional_select(&a, &b, Choice::from(0u8)), + a + ); + assert_eq!( + G1Projective::conditional_select(&a, &b, Choice::from(1u8)), + b + ); +} + +#[test] +fn test_projective_to_affine() { + let a = G1Projective::generator(); + let b = G1Projective::identity(); + + assert!(bool::from(G1Affine::from(a).is_on_curve())); + assert!(!bool::from(G1Affine::from(a).is_identity())); + assert!(bool::from(G1Affine::from(b).is_on_curve())); + assert!(bool::from(G1Affine::from(b).is_identity())); + + let z = Fp::from_raw_unchecked([ + 0xba7a_fa1f_9a6f_e250, + 0xfa0f_5b59_5eaf_e731, + 0x3bdc_4776_94c3_06e7, + 0x2149_be4b_3949_fa24, + 0x64aa_6e06_49b2_078c, + 0x12b1_08ac_3364_3c3e, + ]); + + let c = G1Projective { + x: a.x * z, + y: a.y * z, + z, + }; + + assert_eq!(G1Affine::from(c), G1Affine::generator()); +} + +#[test] +fn test_affine_to_projective() { + let a = G1Affine::generator(); + let b = G1Affine::identity(); + + assert!(bool::from(G1Projective::from(a).is_on_curve())); + assert!(!bool::from(G1Projective::from(a).is_identity())); + assert!(bool::from(G1Projective::from(b).is_on_curve())); + assert!(bool::from(G1Projective::from(b).is_identity())); +} + +#[test] +fn test_doubling() { + { + let tmp = G1Projective::identity().double(); + assert!(bool::from(tmp.is_identity())); + assert!(bool::from(tmp.is_on_curve())); + } + { + let tmp = G1Projective::generator().double(); + assert!(!bool::from(tmp.is_identity())); + assert!(bool::from(tmp.is_on_curve())); + + assert_eq!( + G1Affine::from(tmp), + G1Affine { + x: Fp::from_raw_unchecked([ + 0x53e9_78ce_58a9_ba3c, + 0x3ea0_583c_4f3d_65f9, + 0x4d20_bb47_f001_2960, + 0xa54c_664a_e5b2_b5d9, + 0x26b5_52a3_9d7e_b21f, + 0x0008_895d_26e6_8785, + ]), + y: Fp::from_raw_unchecked([ + 0x7011_0b32_9829_3940, + 0xda33_c539_3f1f_6afc, + 0xb86e_dfd1_6a5a_a785, + 0xaec6_d1c9_e7b1_c895, + 0x25cf_c2b5_22d1_1720, + 0x0636_1c83_f8d0_9b15, + ]), + infinity: Choice::from(0u8) + } + ); + } +} + +#[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([ + 0xba7a_fa1f_9a6f_e250, + 0xfa0f_5b59_5eaf_e731, + 0x3bdc_4776_94c3_06e7, + 0x2149_be4b_3949_fa24, + 0x64aa_6e06_49b2_078c, + 0x12b1_08ac_3364_3c3e, + ]); + + b = G1Projective { + x: b.x * z, + y: b.y * 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([ + 0xba7a_fa1f_9a6f_e250, + 0xfa0f_5b59_5eaf_e731, + 0x3bdc_4776_94c3_06e7, + 0x2149_be4b_3949_fa24, + 0x64aa_6e06_49b2_078c, + 0x12b1_08ac_3364_3c3e, + ]); + + b = G1Projective { + x: b.x * z, + y: b.y * 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 += 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([ + 0xcd03_c9e4_8671_f071, + 0x5dab_2246_1fcd_a5d2, + 0x5870_42af_d385_1b95, + 0x8eb6_0ebe_01ba_cb9e, + 0x03f9_7d6e_83d0_50d2, + 0x18f0_2065_5463_8741, + ]); + 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([ + 0x29e1_e987_ef68_f2d0, + 0xc5f3_ec53_1db0_3233, + 0xacd6_c4b6_ca19_730f, + 0x18ad_9e82_7bc2_bab7, + 0x46e3_b2c5_785c_c7a9, + 0x07e5_71d4_2d22_ddd6, + ]), + y: Fp::from_raw_unchecked([ + 0x94d1_17a7_e5a5_39e7, + 0x8e17_ef67_3d4b_5d22, + 0x9d74_6aaf_508a_33ea, + 0x8c6d_883d_2516_c9a2, + 0x0bc3_b8d5_fb04_47f7, + 0x07bf_a4c7_210f_4f44, + ]), + z: Fp::one() + }) + ); + assert!(!bool::from(c.is_identity())); + assert!(bool::from(c.is_on_curve())); + } +} + +#[test] +fn test_mixed_addition() { + { + let a = G1Affine::identity(); + let b = G1Projective::identity(); + let c = a + b; + assert!(bool::from(c.is_identity())); + assert!(bool::from(c.is_on_curve())); + } + { + let a = G1Affine::identity(); + let mut b = G1Projective::generator(); + { + let z = Fp::from_raw_unchecked([ + 0xba7a_fa1f_9a6f_e250, + 0xfa0f_5b59_5eaf_e731, + 0x3bdc_4776_94c3_06e7, + 0x2149_be4b_3949_fa24, + 0x64aa_6e06_49b2_078c, + 0x12b1_08ac_3364_3c3e, + ]); + + b = G1Projective { + x: b.x * z, + y: b.y * 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 = G1Affine::identity(); + let mut b = G1Projective::generator(); + { + let z = Fp::from_raw_unchecked([ + 0xba7a_fa1f_9a6f_e250, + 0xfa0f_5b59_5eaf_e731, + 0x3bdc_4776_94c3_06e7, + 0x2149_be4b_3949_fa24, + 0x64aa_6e06_49b2_078c, + 0x12b1_08ac_3364_3c3e, + ]); + + b = G1Projective { + x: b.x * z, + y: b.y * 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 += G1Affine::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([ + 0xcd03_c9e4_8671_f071, + 0x5dab_2246_1fcd_a5d2, + 0x5870_42af_d385_1b95, + 0x8eb6_0ebe_01ba_cb9e, + 0x03f9_7d6e_83d0_50d2, + 0x18f0_2065_5463_8741, + ]); + let beta = beta.square(); + let a = G1Projective::generator().double().double(); + let b = G1Projective { + x: a.x * beta, + y: -a.y, + z: a.z, + }; + let a = G1Affine::from(a); + 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([ + 0x29e1_e987_ef68_f2d0, + 0xc5f3_ec53_1db0_3233, + 0xacd6_c4b6_ca19_730f, + 0x18ad_9e82_7bc2_bab7, + 0x46e3_b2c5_785c_c7a9, + 0x07e5_71d4_2d22_ddd6, + ]), + y: Fp::from_raw_unchecked([ + 0x94d1_17a7_e5a5_39e7, + 0x8e17_ef67_3d4b_5d22, + 0x9d74_6aaf_508a_33ea, + 0x8c6d_883d_2516_c9a2, + 0x0bc3_b8d5_fb04_47f7, + 0x07bf_a4c7_210f_4f44, + ]), + z: Fp::one() + }) + ); + assert!(!bool::from(c.is_identity())); + assert!(bool::from(c.is_on_curve())); + } +} + +#[test] +#[allow(clippy::eq_op)] +fn test_projective_negation_and_subtraction() { + let a = G1Projective::generator().double(); + assert_eq!(a + (-a), G1Projective::identity()); + assert_eq!(a + (-a), a - a); +} + +#[test] +fn test_affine_negation_and_subtraction() { + let a = G1Affine::generator(); + assert_eq!(G1Projective::from(a) + (-a), G1Projective::identity()); + assert_eq!(G1Projective::from(a) + (-a), G1Projective::from(a) - a); +} + +#[test] +fn test_projective_scalar_multiplication() { + let g = G1Projective::generator(); + let a = Scalar::from_raw([ + 0x2b56_8297_a56d_a71c, + 0xd8c3_9ecb_0ef3_75d1, + 0x435c_38da_67bf_bf96, + 0x8088_a050_26b6_59b2, + ]); + let b = Scalar::from_raw([ + 0x785f_dd9b_26ef_8b85, + 0xc997_f258_3769_5c18, + 0x4c8d_bc39_e7b7_56c1, + 0x70d9_b6cc_6d87_df20, + ]); + let c = a * b; + + assert_eq!((g * a) * b, g * c); +} + +#[test] +fn test_affine_scalar_multiplication() { + let g = G1Affine::generator(); + let a = Scalar::from_raw([ + 0x2b56_8297_a56d_a71c, + 0xd8c3_9ecb_0ef3_75d1, + 0x435c_38da_67bf_bf96, + 0x8088_a050_26b6_59b2, + ]); + let b = Scalar::from_raw([ + 0x785f_dd9b_26ef_8b85, + 0xc997_f258_3769_5c18, + 0x4c8d_bc39_e7b7_56c1, + 0x70d9_b6cc_6d87_df20, + ]); + let c = a * b; + + assert_eq!(G1Affine::from(g * a) * b, g * c); +} + +#[test] +fn test_is_torsion_free() { + let a = G1Affine { + x: Fp::from_raw_unchecked([ + 0x0aba_f895_b97e_43c8, + 0xba4c_6432_eb9b_61b0, + 0x1250_6f52_adfe_307f, + 0x7502_8c34_3933_6b72, + 0x8474_4f05_b8e9_bd71, + 0x113d_554f_b095_54f7, + ]), + y: Fp::from_raw_unchecked([ + 0x73e9_0e88_f5cf_01c0, + 0x3700_7b65_dd31_97e2, + 0x5cf9_a199_2f0d_7c78, + 0x4f83_c10b_9eb3_330d, + 0xf6a6_3f6f_07f6_0961, + 0x0c53_b5b9_7e63_4df3, + ]), + infinity: Choice::from(0u8), + }; + assert!(!bool::from(a.is_torsion_free())); + + assert!(bool::from(G1Affine::identity().is_torsion_free())); + assert!(bool::from(G1Affine::generator().is_torsion_free())); +} + +#[test] +fn test_mul_by_x() { + // multiplying by `x` a point in G1 is the same as multiplying by + // the equivalent scalar. + let generator = G1Projective::generator(); + let x = if crate::BLS_X_IS_NEGATIVE { + -Scalar::from(crate::BLS_X) + } else { + Scalar::from(crate::BLS_X) + }; + assert_eq!(generator.mul_by_x(), generator * x); + + let point = G1Projective::generator() * Scalar::from(42); + assert_eq!(point.mul_by_x(), point * x); +} + +#[test] +fn test_clear_cofactor() { + // the generator (and the identity) are always on the curve, + // even after clearing the cofactor + let generator = G1Projective::generator(); + assert!(bool::from(generator.clear_cofactor().is_on_curve())); + let id = G1Projective::identity(); + assert!(bool::from(id.clear_cofactor().is_on_curve())); + + let z = Fp::from_raw_unchecked([ + 0x3d2d1c670671394e, + 0x0ee3a800a2f7c1ca, + 0x270f4f21da2e5050, + 0xe02840a53f1be768, + 0x55debeb597512690, + 0x08bd25353dc8f791, + ]); + + let point = G1Projective { + x: Fp::from_raw_unchecked([ + 0x48af5ff540c817f0, + 0xd73893acaf379d5a, + 0xe6c43584e18e023c, + 0x1eda39c30f188b3e, + 0xf618c6d3ccc0f8d8, + 0x0073542cd671e16c, + ]) * z, + y: Fp::from_raw_unchecked([ + 0x57bf8be79461d0ba, + 0xfc61459cee3547c3, + 0x0d23567df1ef147b, + 0x0ee187bcce1d9b64, + 0xb0c8cfbe9dc8fdc1, + 0x1328661767ef368b, + ]), + z: z.square() * z, + }; + + assert!(bool::from(point.is_on_curve())); + assert!(!bool::from(G1Affine::from(point).is_torsion_free())); + let cleared_point = point.clear_cofactor(); + assert!(bool::from(cleared_point.is_on_curve())); + assert!(bool::from(G1Affine::from(cleared_point).is_torsion_free())); + + // in BLS12-381 the cofactor in G1 can be + // cleared multiplying by (1-x) + let h_eff = Scalar::from(1) + Scalar::from(crate::BLS_X); + assert_eq!(point.clear_cofactor(), point * h_eff); +} + +#[test] +fn test_batch_normalize() { + let a = G1Projective::generator().double(); + let b = a.double(); + let c = b.double(); + + for a_identity in (0..=1).map(|n| n == 1) { + for b_identity in (0..=1).map(|n| n == 1) { + for c_identity in (0..=1).map(|n| n == 1) { + let mut v = [a, b, c]; + if a_identity { + v[0] = G1Projective::identity() + } + if b_identity { + v[1] = G1Projective::identity() + } + if c_identity { + v[2] = G1Projective::identity() + } + + let mut t = [ + G1Affine::identity(), + G1Affine::identity(), + G1Affine::identity(), + ]; + let expected = [ + G1Affine::from(v[0]), + G1Affine::from(v[1]), + G1Affine::from(v[2]), + ]; + + G1Projective::batch_normalize(&v[..], &mut t[..]); + + assert_eq!(&t[..], &expected[..]); + } + } + } +} + +#[cfg(feature = "zeroize")] +#[test] +fn test_zeroize() { + use zeroize::Zeroize; + + let mut a = G1Affine::generator(); + a.zeroize(); + assert!(bool::from(a.is_identity())); + + let mut a = G1Projective::generator(); + a.zeroize(); + assert!(bool::from(a.is_identity())); + + let mut a = GroupEncoding::to_bytes(&G1Affine::generator()); + a.zeroize(); + assert_eq!(&a, &G1Compressed::default()); + + let mut a = UncompressedEncoding::to_uncompressed(&G1Affine::generator()); + a.zeroize(); + assert_eq!(&a, &G1Uncompressed::default()); +} + +#[test] +fn test_commutative_scalar_subgroup_multiplication() { + let a = Scalar::from_raw([ + 0x1fff_3231_233f_fffd, + 0x4884_b7fa_0003_4802, + 0x998c_4fef_ecbc_4ff3, + 0x1824_b159_acc5_0562, + ]); + + let g1_a = G1Affine::generator(); + let g1_p = G1Projective::generator(); + + // By reference. + assert_eq!(&g1_a * &a, &a * &g1_a); + assert_eq!(&g1_p * &a, &a * &g1_p); + + // Mixed + assert_eq!(&g1_a * a.clone(), a.clone() * &g1_a); + assert_eq!(&g1_p * a.clone(), a.clone() * &g1_p); + assert_eq!(g1_a.clone() * &a, &a * g1_a.clone()); + assert_eq!(g1_p.clone() * &a, &a * g1_p.clone()); + + // By value. + assert_eq!(g1_p * a, a * g1_p); + assert_eq!(g1_a * a, a * g1_a); +} diff --git a/constantine/bls12_381/src/g2.rs b/constantine/bls12_381/src/g2.rs new file mode 100644 index 000000000..86eafcbc6 --- /dev/null +++ b/constantine/bls12_381/src/g2.rs @@ -0,0 +1,2180 @@ +//! This module provides an implementation of the $\mathbb{G}_2$ group of BLS12-381. +#![allow(clippy::all)] + +use core::borrow::Borrow; +use core::fmt; +use core::iter::Sum; +use core::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}; +use group::{ + prime::{PrimeCurve, PrimeCurveAffine, PrimeGroup}, + Curve, Group, GroupEncoding, UncompressedEncoding, +}; +use rand_core::RngCore; +use subtle::{Choice, ConditionallySelectable, ConstantTimeEq, CtOption}; + +#[cfg(feature = "alloc")] +use group::WnafGroup; + +use crate::fp::Fp; +use crate::fp2::Fp2; +use crate::Scalar; + +/// 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 +/// improve performance through the use of mixed curve model arithmetic. +/// +/// Values of `G2Affine` are guaranteed to be in the $q$-order subgroup unless an +/// "unchecked" API was misused. +#[cfg_attr(docsrs, doc(cfg(feature = "groups")))] +#[derive(Copy, Clone, Debug)] +pub struct G2Affine { + pub(crate) x: Fp2, + pub(crate) y: Fp2, + infinity: Choice, +} + +impl Default for G2Affine { + fn default() -> G2Affine { + G2Affine::identity() + } +} + +#[cfg(feature = "zeroize")] +impl zeroize::DefaultIsZeroes for G2Affine {} + +impl fmt::Display for G2Affine { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +impl<'a> From<&'a G2Projective> for G2Affine { + fn from(p: &'a G2Projective) -> G2Affine { + let zinv = p.z.invert().unwrap_or(Fp2::zero()); + let x = p.x * zinv; + let y = p.y * zinv; + + let tmp = G2Affine { + x, + y, + infinity: Choice::from(0u8), + }; + + G2Affine::conditional_select(&tmp, &G2Affine::identity(), zinv.is_zero()) + } +} + +impl From for G2Affine { + fn from(p: G2Projective) -> G2Affine { + G2Affine::from(&p) + } +} + +impl ConstantTimeEq for G2Affine { + fn ct_eq(&self, other: &Self) -> Choice { + // The only cases in which two points are equal are + // 1. infinity is set on both + // 2. infinity is not set on both, and their coordinates are equal + + (self.infinity & other.infinity) + | ((!self.infinity) + & (!other.infinity) + & self.x.ct_eq(&other.x) + & self.y.ct_eq(&other.y)) + } +} + +impl ConditionallySelectable for G2Affine { + fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self { + G2Affine { + x: Fp2::conditional_select(&a.x, &b.x, choice), + y: Fp2::conditional_select(&a.y, &b.y, choice), + infinity: Choice::conditional_select(&a.infinity, &b.infinity, choice), + } + } +} + +impl Eq for G2Affine {} +impl PartialEq for G2Affine { + #[inline] + fn eq(&self, other: &Self) -> bool { + bool::from(self.ct_eq(other)) + } +} + +impl<'a> Neg for &'a G2Affine { + type Output = G2Affine; + + #[inline] + fn neg(self) -> G2Affine { + G2Affine { + x: self.x, + y: Fp2::conditional_select(&-self.y, &Fp2::one(), self.infinity), + infinity: self.infinity, + } + } +} + +impl Neg for G2Affine { + type Output = G2Affine; + + #[inline] + fn neg(self) -> G2Affine { + -&self + } +} + +impl<'a, 'b> Add<&'b G2Projective> for &'a G2Affine { + type Output = G2Projective; + + #[inline] + fn add(self, rhs: &'b G2Projective) -> G2Projective { + rhs.add_mixed(self) + } +} + +impl<'a, 'b> Add<&'b G2Affine> for &'a G2Projective { + type Output = G2Projective; + + #[inline] + fn add(self, rhs: &'b G2Affine) -> G2Projective { + self.add_mixed(rhs) + } +} + +impl<'a, 'b> Sub<&'b G2Projective> for &'a G2Affine { + type Output = G2Projective; + + #[inline] + fn sub(self, rhs: &'b G2Projective) -> G2Projective { + self + (-rhs) + } +} + +impl<'a, 'b> Sub<&'b G2Affine> for &'a G2Projective { + type Output = G2Projective; + + #[inline] + fn sub(self, rhs: &'b G2Affine) -> G2Projective { + self + (-rhs) + } +} + +impl Sum for G2Projective +where + T: Borrow, +{ + fn sum(iter: I) -> Self + where + I: Iterator, + { + iter.fold(Self::identity(), |acc, item| acc + item.borrow()) + } +} + +impl_binops_additive!(G2Projective, G2Affine); +impl_binops_additive_specify_output!(G2Affine, G2Projective, G2Projective); + +const B: Fp2 = Fp2 { + c0: Fp::from_raw_unchecked([ + 0xaa27_0000_000c_fff3, + 0x53cc_0032_fc34_000a, + 0x478f_e97a_6b0a_807f, + 0xb1d3_7ebe_e6ba_24d7, + 0x8ec9_733b_bf78_ab2f, + 0x09d6_4551_3d83_de7e, + ]), + c1: Fp::from_raw_unchecked([ + 0xaa27_0000_000c_fff3, + 0x53cc_0032_fc34_000a, + 0x478f_e97a_6b0a_807f, + 0xb1d3_7ebe_e6ba_24d7, + 0x8ec9_733b_bf78_ab2f, + 0x09d6_4551_3d83_de7e, + ]), +}; + +const B3: Fp2 = Fp2::add(&Fp2::add(&B, &B), &B); + +impl G2Affine { + /// Returns the identity of the group: the point at infinity. + pub fn identity() -> G2Affine { + G2Affine { + x: Fp2::zero(), + y: Fp2::one(), + infinity: Choice::from(1u8), + } + } + + /// Returns a fixed generator of the group. See [`notes::design`](notes/design/index.html#fixed-generators) + /// for how this generator is chosen. + pub fn generator() -> G2Affine { + G2Affine { + x: Fp2 { + c0: Fp::from_raw_unchecked([ + 0xf5f2_8fa2_0294_0a10, + 0xb3f5_fb26_87b4_961a, + 0xa1a8_93b5_3e2a_e580, + 0x9894_999d_1a3c_aee9, + 0x6f67_b763_1863_366b, + 0x0581_9192_4350_bcd7, + ]), + c1: Fp::from_raw_unchecked([ + 0xa5a9_c075_9e23_f606, + 0xaaa0_c59d_bccd_60c3, + 0x3bb1_7e18_e286_7806, + 0x1b1a_b6cc_8541_b367, + 0xc2b6_ed0e_f215_8547, + 0x1192_2a09_7360_edf3, + ]), + }, + y: Fp2 { + c0: Fp::from_raw_unchecked([ + 0x4c73_0af8_6049_4c4a, + 0x597c_fa1f_5e36_9c5a, + 0xe7e6_856c_aa0a_635a, + 0xbbef_b5e9_6e0d_495f, + 0x07d3_a975_f0ef_25a2, + 0x0083_fd8e_7e80_dae5, + ]), + c1: Fp::from_raw_unchecked([ + 0xadc0_fc92_df64_b05d, + 0x18aa_270a_2b14_61dc, + 0x86ad_ac6a_3be4_eba0, + 0x7949_5c4e_c93d_a33a, + 0xe717_5850_a43c_caed, + 0x0b2b_c2a1_63de_1bf2, + ]), + }, + infinity: Choice::from(0u8), + } + } + + /// Serializes this element into compressed form. See [`notes::serialization`](crate::notes::serialization) + /// for details about how group elements are serialized. + pub fn to_compressed(&self) -> [u8; 96] { + // Strictly speaking, self.x is zero already when self.infinity is true, but + // to guard against implementation mistakes we do not assume this. + let x = Fp2::conditional_select(&self.x, &Fp2::zero(), self.infinity); + + let mut res = [0; 96]; + + (&mut res[0..48]).copy_from_slice(&x.c1.to_bytes()[..]); + (&mut res[48..96]).copy_from_slice(&x.c0.to_bytes()[..]); + + // This point is in compressed form, so we set the most significant bit. + res[0] |= 1u8 << 7; + + // Is this point at infinity? If so, set the second-most significant bit. + res[0] |= u8::conditional_select(&0u8, &(1u8 << 6), self.infinity); + + // Is the y-coordinate the lexicographically largest of the two associated with the + // x-coordinate? If so, set the third-most significant bit so long as this is not + // the point at infinity. + res[0] |= u8::conditional_select( + &0u8, + &(1u8 << 5), + (!self.infinity) & self.y.lexicographically_largest(), + ); + + res + } + + /// Serializes this element into uncompressed form. See [`notes::serialization`](crate::notes::serialization) + /// for details about how group elements are serialized. + pub fn to_uncompressed(&self) -> [u8; 192] { + let mut res = [0; 192]; + + let x = Fp2::conditional_select(&self.x, &Fp2::zero(), self.infinity); + let y = Fp2::conditional_select(&self.y, &Fp2::zero(), self.infinity); + + res[0..48].copy_from_slice(&x.c1.to_bytes()[..]); + res[48..96].copy_from_slice(&x.c0.to_bytes()[..]); + res[96..144].copy_from_slice(&y.c1.to_bytes()[..]); + res[144..192].copy_from_slice(&y.c0.to_bytes()[..]); + + // Is this point at infinity? If so, set the second-most significant bit. + res[0] |= u8::conditional_select(&0u8, &(1u8 << 6), self.infinity); + + res + } + + /// Attempts to deserialize an uncompressed element. See [`notes::serialization`](crate::notes::serialization) + /// for details about how group elements are serialized. + pub fn from_uncompressed(bytes: &[u8; 192]) -> CtOption { + Self::from_uncompressed_unchecked(bytes) + .and_then(|p| CtOption::new(p, p.is_on_curve() & p.is_torsion_free())) + } + + /// Attempts to deserialize an uncompressed element, not checking if the + /// element is on the curve and not checking if it is in the correct subgroup. + /// **This is dangerous to call unless you trust the bytes you are reading; otherwise, + /// API invariants may be broken.** Please consider using `from_uncompressed()` instead. + pub fn from_uncompressed_unchecked(bytes: &[u8; 192]) -> CtOption { + // Obtain the three flags from the start of the byte sequence + let compression_flag_set = Choice::from((bytes[0] >> 7) & 1); + let infinity_flag_set = Choice::from((bytes[0] >> 6) & 1); + let sort_flag_set = Choice::from((bytes[0] >> 5) & 1); + + // Attempt to obtain the x-coordinate + let xc1 = { + let mut tmp = [0; 48]; + tmp.copy_from_slice(&bytes[0..48]); + + // Mask away the flag bits + tmp[0] &= 0b0001_1111; + + Fp::from_bytes(&tmp) + }; + let xc0 = { + let mut tmp = [0; 48]; + tmp.copy_from_slice(&bytes[48..96]); + + Fp::from_bytes(&tmp) + }; + + // Attempt to obtain the y-coordinate + let yc1 = { + let mut tmp = [0; 48]; + tmp.copy_from_slice(&bytes[96..144]); + + Fp::from_bytes(&tmp) + }; + let yc0 = { + let mut tmp = [0; 48]; + tmp.copy_from_slice(&bytes[144..192]); + + Fp::from_bytes(&tmp) + }; + + xc1.and_then(|xc1| { + xc0.and_then(|xc0| { + yc1.and_then(|yc1| { + yc0.and_then(|yc0| { + let x = Fp2 { + c0: xc0, + c1: xc1 + }; + let y = Fp2 { + c0: yc0, + c1: yc1 + }; + + // Create a point representing this value + let p = G2Affine::conditional_select( + &G2Affine { + x, + y, + infinity: infinity_flag_set, + }, + &G2Affine::identity(), + infinity_flag_set, + ); + + CtOption::new( + p, + // If the infinity flag is set, the x and y coordinates should have been zero. + ((!infinity_flag_set) | (infinity_flag_set & x.is_zero() & y.is_zero())) & + // The compression flag should not have been set, as this is an uncompressed element + (!compression_flag_set) & + // The sort flag should not have been set, as this is an uncompressed element + (!sort_flag_set), + ) + }) + }) + }) + }) + } + + /// Attempts to deserialize a compressed element. See [`notes::serialization`](crate::notes::serialization) + /// for details about how group elements are serialized. + pub fn from_compressed(bytes: &[u8; 96]) -> CtOption { + // We already know the point is on the curve because this is established + // by the y-coordinate recovery procedure in from_compressed_unchecked(). + + Self::from_compressed_unchecked(bytes).and_then(|p| CtOption::new(p, p.is_torsion_free())) + } + + /// Attempts to deserialize an uncompressed element, not checking if the + /// element is in the correct subgroup. + /// **This is dangerous to call unless you trust the bytes you are reading; otherwise, + /// API invariants may be broken.** Please consider using `from_compressed()` instead. + pub fn from_compressed_unchecked(bytes: &[u8; 96]) -> CtOption { + // Obtain the three flags from the start of the byte sequence + let compression_flag_set = Choice::from((bytes[0] >> 7) & 1); + let infinity_flag_set = Choice::from((bytes[0] >> 6) & 1); + let sort_flag_set = Choice::from((bytes[0] >> 5) & 1); + + // Attempt to obtain the x-coordinate + let xc1 = { + let mut tmp = [0; 48]; + tmp.copy_from_slice(&bytes[0..48]); + + // Mask away the flag bits + tmp[0] &= 0b0001_1111; + + Fp::from_bytes(&tmp) + }; + let xc0 = { + let mut tmp = [0; 48]; + tmp.copy_from_slice(&bytes[48..96]); + + Fp::from_bytes(&tmp) + }; + + xc1.and_then(|xc1| { + xc0.and_then(|xc0| { + let x = Fp2 { c0: xc0, c1: xc1 }; + + // If the infinity flag is set, return the value assuming + // the x-coordinate is zero and the sort bit is not set. + // + // Otherwise, return a recovered point (assuming the correct + // y-coordinate can be found) so long as the infinity flag + // was not set. + CtOption::new( + G2Affine::identity(), + infinity_flag_set & // Infinity flag should be set + compression_flag_set & // Compression flag should be set + (!sort_flag_set) & // Sort flag should not be set + x.is_zero(), // The x-coordinate should be zero + ) + .or_else(|| { + // Recover a y-coordinate given x by y = sqrt(x^3 + 4) + ((x.square() * x) + B).sqrt().and_then(|y| { + // Switch to the correct y-coordinate if necessary. + let y = Fp2::conditional_select( + &y, + &-y, + y.lexicographically_largest() ^ sort_flag_set, + ); + + CtOption::new( + G2Affine { + x, + y, + infinity: infinity_flag_set, + }, + (!infinity_flag_set) & // Infinity flag should not be set + compression_flag_set, // Compression flag should be set + ) + }) + }) + }) + }) + } + + /// Returns true if this element is the identity (the point at infinity). + #[inline] + pub fn is_identity(&self) -> Choice { + self.infinity + } + + /// Returns true if this point is free of an $h$-torsion component, and so it + /// exists within the $q$-order subgroup $\mathbb{G}_2$. This should always return true + /// unless an "unchecked" API was used. + pub fn is_torsion_free(&self) -> Choice { + // Algorithm from Section 4 of https://eprint.iacr.org/2021/1130 + // Updated proof of correctness in https://eprint.iacr.org/2022/352 + // + // Check that psi(P) == [x] P + let p = G2Projective::from(self); + p.psi().ct_eq(&p.mul_by_x()) + } + + /// Returns true if this point is on the curve. This should always return + /// true unless an "unchecked" API was used. + pub fn is_on_curve(&self) -> Choice { + // y^2 - x^3 ?= 4(u + 1) + (self.y.square() - (self.x.square() * self.x)).ct_eq(&B) | self.infinity + } +} + +/// This is an element of $\mathbb{G}_2$ represented in the projective coordinate space. +#[cfg_attr(docsrs, doc(cfg(feature = "groups")))] +#[derive(Copy, Clone, Debug)] +pub struct G2Projective { + pub x: Fp2, + pub y: Fp2, + pub z: Fp2, +} + +impl Default for G2Projective { + fn default() -> G2Projective { + G2Projective::identity() + } +} + +#[cfg(feature = "zeroize")] +impl zeroize::DefaultIsZeroes for G2Projective {} + +impl fmt::Display for G2Projective { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +impl<'a> From<&'a G2Affine> for G2Projective { + fn from(p: &'a G2Affine) -> G2Projective { + G2Projective { + x: p.x, + y: p.y, + z: Fp2::conditional_select(&Fp2::one(), &Fp2::zero(), p.infinity), + } + } +} + +impl From for G2Projective { + fn from(p: G2Affine) -> G2Projective { + G2Projective::from(&p) + } +} + +impl ConstantTimeEq for G2Projective { + fn ct_eq(&self, other: &Self) -> Choice { + // Is (xz, yz, z) equal to (x'z', y'z', z') when converted to affine? + + let x1 = self.x * other.z; + let x2 = other.x * self.z; + + let y1 = self.y * other.z; + let y2 = other.y * self.z; + + let self_is_zero = self.z.is_zero(); + let other_is_zero = other.z.is_zero(); + + (self_is_zero & other_is_zero) // Both point at infinity + | ((!self_is_zero) & (!other_is_zero) & x1.ct_eq(&x2) & y1.ct_eq(&y2)) + // Neither point at infinity, coordinates are the same + } +} + +impl ConditionallySelectable for G2Projective { + fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self { + G2Projective { + x: Fp2::conditional_select(&a.x, &b.x, choice), + y: Fp2::conditional_select(&a.y, &b.y, choice), + z: Fp2::conditional_select(&a.z, &b.z, choice), + } + } +} + +impl Eq for G2Projective {} +impl PartialEq for G2Projective { + #[inline] + fn eq(&self, other: &Self) -> bool { + bool::from(self.ct_eq(other)) + } +} + +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<'a, 'b> Mul<&'b Scalar> for &'a G2Projective { + type Output = G2Projective; + + fn mul(self, other: &'b Scalar) -> Self::Output { + self.multiply(&other.to_bytes()) + } +} + +impl<'a, 'b> Mul<&'b G2Projective> for &'a Scalar { + type Output = G2Projective; + + #[inline] + fn mul(self, rhs: &'b G2Projective) -> Self::Output { + rhs * self + } +} + +impl<'a, 'b> Mul<&'b Scalar> for &'a G2Affine { + type Output = G2Projective; + + fn mul(self, other: &'b Scalar) -> Self::Output { + G2Projective::from(self).multiply(&other.to_bytes()) + } +} + +impl<'a, 'b> Mul<&'b G2Affine> for &'a Scalar { + type Output = G2Projective; + + #[inline] + fn mul(self, rhs: &'b G2Affine) -> Self::Output { + rhs * self + } +} + +impl_binops_additive!(G2Projective, G2Projective); +impl_binops_multiplicative!(G2Projective, Scalar); +impl_binops_multiplicative_mixed!(G2Affine, Scalar, G2Projective); +impl_binops_multiplicative_mixed!(Scalar, G2Affine, G2Projective); +impl_binops_multiplicative_mixed!(Scalar, G2Projective, G2Projective); + +#[inline(always)] +fn mul_by_3b(x: Fp2) -> Fp2 { + x * B3 +} + +impl G2Projective { + /// Returns the identity of the group: the point at infinity. + pub fn identity() -> G2Projective { + G2Projective { + x: Fp2::zero(), + y: Fp2::one(), + z: Fp2::zero(), + } + } + + /// Returns a fixed generator of the group. See [`notes::design`](notes/design/index.html#fixed-generators) + /// for how this generator is chosen. + pub fn generator() -> G2Projective { + G2Projective { + x: Fp2 { + c0: Fp::from_raw_unchecked([ + 0xf5f2_8fa2_0294_0a10, + 0xb3f5_fb26_87b4_961a, + 0xa1a8_93b5_3e2a_e580, + 0x9894_999d_1a3c_aee9, + 0x6f67_b763_1863_366b, + 0x0581_9192_4350_bcd7, + ]), + c1: Fp::from_raw_unchecked([ + 0xa5a9_c075_9e23_f606, + 0xaaa0_c59d_bccd_60c3, + 0x3bb1_7e18_e286_7806, + 0x1b1a_b6cc_8541_b367, + 0xc2b6_ed0e_f215_8547, + 0x1192_2a09_7360_edf3, + ]), + }, + y: Fp2 { + c0: Fp::from_raw_unchecked([ + 0x4c73_0af8_6049_4c4a, + 0x597c_fa1f_5e36_9c5a, + 0xe7e6_856c_aa0a_635a, + 0xbbef_b5e9_6e0d_495f, + 0x07d3_a975_f0ef_25a2, + 0x0083_fd8e_7e80_dae5, + ]), + c1: Fp::from_raw_unchecked([ + 0xadc0_fc92_df64_b05d, + 0x18aa_270a_2b14_61dc, + 0x86ad_ac6a_3be4_eba0, + 0x7949_5c4e_c93d_a33a, + 0xe717_5850_a43c_caed, + 0x0b2b_c2a1_63de_1bf2, + ]), + }, + z: Fp2::one(), + } + } + + /// Computes the doubling of this point. + pub fn double(&self) -> G2Projective { + // Algorithm 9, https://eprint.iacr.org/2015/1060.pdf + + let t0 = self.y.square(); + let z3 = t0 + t0; + let z3 = z3 + z3; + let z3 = z3 + z3; + let t1 = self.y * self.z; + let t2 = self.z.square(); + let t2 = mul_by_3b(t2); + let x3 = t2 * z3; + let y3 = t0 + t2; + let z3 = t1 * z3; + let t1 = t2 + t2; + let t2 = t1 + t2; + let t0 = t0 - t2; + let y3 = t0 * y3; + let y3 = x3 + y3; + let t1 = self.x * self.y; + let x3 = t0 * t1; + let x3 = x3 + x3; + + let tmp = G2Projective { + x: x3, + y: y3, + z: z3, + }; + + G2Projective::conditional_select(&tmp, &G2Projective::identity(), self.is_identity()) + } + + /// Adds this point to another point. + pub fn add(&self, rhs: &G2Projective) -> G2Projective { + // Algorithm 7, https://eprint.iacr.org/2015/1060.pdf + + let t0 = self.x * rhs.x; + let t1 = self.y * rhs.y; + let t2 = self.z * rhs.z; + let t3 = self.x + self.y; + let t4 = rhs.x + rhs.y; + let t3 = t3 * t4; + let t4 = t0 + t1; + let t3 = t3 - t4; + let t4 = self.y + self.z; + let x3 = rhs.y + rhs.z; + let t4 = t4 * x3; + let x3 = t1 + t2; + let t4 = t4 - x3; + let x3 = self.x + self.z; + let y3 = rhs.x + rhs.z; + let x3 = x3 * y3; + let y3 = t0 + t2; + let y3 = x3 - y3; + let x3 = t0 + t0; + let t0 = x3 + t0; + let t2 = mul_by_3b(t2); + let z3 = t1 + t2; + let t1 = t1 - t2; + let y3 = mul_by_3b(y3); + let x3 = t4 * y3; + let t2 = t3 * t1; + let x3 = t2 - x3; + let y3 = y3 * t0; + let t1 = t1 * z3; + let y3 = t1 + y3; + let t0 = t0 * t3; + let z3 = z3 * t4; + let z3 = z3 + t0; + + G2Projective { + x: x3, + y: y3, + z: z3, + } + } + + /// Adds this point to another point in the affine model. + pub fn add_mixed(&self, rhs: &G2Affine) -> G2Projective { + // Algorithm 8, https://eprint.iacr.org/2015/1060.pdf + + let t0 = self.x * rhs.x; + let t1 = self.y * rhs.y; + let t3 = rhs.x + rhs.y; + let t4 = self.x + self.y; + let t3 = t3 * t4; + let t4 = t0 + t1; + let t3 = t3 - t4; + let t4 = rhs.y * self.z; + let t4 = t4 + self.y; + let y3 = rhs.x * self.z; + let y3 = y3 + self.x; + let x3 = t0 + t0; + let t0 = x3 + t0; + let t2 = mul_by_3b(self.z); + let z3 = t1 + t2; + let t1 = t1 - t2; + let y3 = mul_by_3b(y3); + let x3 = t4 * y3; + let t2 = t3 * t1; + let x3 = t2 - x3; + let y3 = y3 * t0; + let t1 = t1 * z3; + let y3 = t1 + y3; + let t0 = t0 * t3; + let z3 = z3 * t4; + let z3 = z3 + t0; + + let tmp = G2Projective { + x: x3, + y: y3, + z: z3, + }; + + G2Projective::conditional_select(&tmp, self, rhs.is_identity()) + } + + fn multiply(&self, by: &[u8]) -> G2Projective { + let mut acc = G2Projective::identity(); + + // This is a simple double-and-add implementation of point + // multiplication, moving from most significant to least + // significant bit of the scalar. + // + // We skip the leading bit because it's always unset for Fq + // elements. + for bit in by + .iter() + .rev() + .flat_map(|byte| (0..8).rev().map(move |i| Choice::from((byte >> i) & 1u8))) + .skip(1) + { + acc = acc.double(); + acc = G2Projective::conditional_select(&acc, &(acc + self), bit); + } + + acc + } + + fn psi(&self) -> G2Projective { + // 1 / ((u+1) ^ ((q-1)/3)) + let psi_coeff_x = Fp2 { + c0: Fp::zero(), + c1: Fp::from_raw_unchecked([ + 0x890dc9e4867545c3, + 0x2af322533285a5d5, + 0x50880866309b7e2c, + 0xa20d1b8c7e881024, + 0x14e4f04fe2db9068, + 0x14e56d3f1564853a, + ]), + }; + // 1 / ((u+1) ^ (p-1)/2) + let psi_coeff_y = Fp2 { + c0: Fp::from_raw_unchecked([ + 0x3e2f585da55c9ad1, + 0x4294213d86c18183, + 0x382844c88b623732, + 0x92ad2afd19103e18, + 0x1d794e4fac7cf0b9, + 0x0bd592fc7d825ec8, + ]), + c1: Fp::from_raw_unchecked([ + 0x7bcfa7a25aa30fda, + 0xdc17dec12a927e7c, + 0x2f088dd86b4ebef1, + 0xd1ca2087da74d4a7, + 0x2da2596696cebc1d, + 0x0e2b7eedbbfd87d2, + ]), + }; + + G2Projective { + // x = frobenius(x)/((u+1)^((p-1)/3)) + x: self.x.frobenius_map() * psi_coeff_x, + // y = frobenius(y)/(u+1)^((p-1)/2) + y: self.y.frobenius_map() * psi_coeff_y, + // z = frobenius(z) + z: self.z.frobenius_map(), + } + } + + fn psi2(&self) -> G2Projective { + // 1 / 2 ^ ((q-1)/3) + let psi2_coeff_x = Fp2 { + c0: Fp::from_raw_unchecked([ + 0xcd03c9e48671f071, + 0x5dab22461fcda5d2, + 0x587042afd3851b95, + 0x8eb60ebe01bacb9e, + 0x03f97d6e83d050d2, + 0x18f0206554638741, + ]), + c1: Fp::zero(), + }; + + G2Projective { + // x = frobenius^2(x)/2^((p-1)/3); note that q^2 is the order of the field. + x: self.x * psi2_coeff_x, + // y = -frobenius^2(y); note that q^2 is the order of the field. + y: self.y.neg(), + // z = z + z: self.z, + } + } + + /// Multiply `self` by `crate::BLS_X`, using double and add. + fn mul_by_x(&self) -> G2Projective { + let mut xself = G2Projective::identity(); + // NOTE: in BLS12-381 we can just skip the first bit. + let mut x = crate::BLS_X >> 1; + let mut acc = *self; + while x != 0 { + acc = acc.double(); + if x % 2 == 1 { + xself += acc; + } + x >>= 1; + } + // finally, flip the sign + if crate::BLS_X_IS_NEGATIVE { + xself = -xself; + } + xself + } + + /// Clears the cofactor, using [Budroni-Pintore](https://ia.cr/2017/419). + /// This is equivalent to multiplying by $h\_\textrm{eff} = 3(z^2 - 1) \cdot + /// h_2$, where $h_2$ is the cofactor of $\mathbb{G}\_2$ and $z$ is the + /// parameter of BLS12-381. + pub fn clear_cofactor(&self) -> G2Projective { + let t1 = self.mul_by_x(); // [x] P + let t2 = self.psi(); // psi(P) + + self.double().psi2() // psi^2(2P) + + (t1 + t2).mul_by_x() // psi^2(2P) + [x^2] P + [x] psi(P) + - t1 // psi^2(2P) + [x^2 - x] P + [x] psi(P) + - t2 // psi^2(2P) + [x^2 - x] P + [x - 1] psi(P) + - self // psi^2(2P) + [x^2 - x - 1] P + [x - 1] psi(P) + } + + /// Converts a batch of `G2Projective` elements into `G2Affine` elements. This + /// function will panic if `p.len() != q.len()`. + pub fn batch_normalize(p: &[Self], q: &mut [G2Affine]) { + assert_eq!(p.len(), q.len()); + + let mut acc = Fp2::one(); + for (p, q) in p.iter().zip(q.iter_mut()) { + // We use the `x` field of `G2Affine` to store the product + // of previous z-coordinates seen. + q.x = acc; + + // We will end up skipping all identities in p + acc = Fp2::conditional_select(&(acc * p.z), &acc, p.is_identity()); + } + + // This is the inverse, as all z-coordinates are nonzero and the ones + // that are not are skipped. + acc = acc.invert().unwrap(); + + for (p, q) in p.iter().rev().zip(q.iter_mut().rev()) { + let skip = p.is_identity(); + + // Compute tmp = 1/z + let tmp = q.x * acc; + + // Cancel out z-coordinate in denominator of `acc` + acc = Fp2::conditional_select(&(acc * p.z), &acc, skip); + + // Set the coordinates to the correct value + q.x = p.x * tmp; + q.y = p.y * tmp; + q.infinity = Choice::from(0u8); + + *q = G2Affine::conditional_select(q, &G2Affine::identity(), skip); + } + } + + /// Returns true if this element is the identity (the point at infinity). + #[inline] + pub fn is_identity(&self) -> Choice { + self.z.is_zero() + } + + /// Returns true if this point is on the curve. This should always return + /// true unless an "unchecked" API was used. + pub fn is_on_curve(&self) -> Choice { + // Y^2 Z = X^3 + b Z^3 + + (self.y.square() * self.z).ct_eq(&(self.x.square() * self.x + self.z.square() * self.z * B)) + | self.z.is_zero() + } +} + +#[derive(Clone, Copy)] +pub struct G2Compressed([u8; 96]); + +impl fmt::Debug for G2Compressed { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0[..].fmt(f) + } +} + +impl Default for G2Compressed { + fn default() -> Self { + G2Compressed([0; 96]) + } +} + +#[cfg(feature = "zeroize")] +impl zeroize::DefaultIsZeroes for G2Compressed {} + +impl AsRef<[u8]> for G2Compressed { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl AsMut<[u8]> for G2Compressed { + fn as_mut(&mut self) -> &mut [u8] { + &mut self.0 + } +} + +impl ConstantTimeEq for G2Compressed { + fn ct_eq(&self, other: &Self) -> Choice { + self.0.ct_eq(&other.0) + } +} + +impl Eq for G2Compressed {} +impl PartialEq for G2Compressed { + #[inline] + fn eq(&self, other: &Self) -> bool { + bool::from(self.ct_eq(other)) + } +} + +#[derive(Clone, Copy)] +pub struct G2Uncompressed([u8; 192]); + +impl fmt::Debug for G2Uncompressed { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0[..].fmt(f) + } +} + +impl Default for G2Uncompressed { + fn default() -> Self { + G2Uncompressed([0; 192]) + } +} + +#[cfg(feature = "zeroize")] +impl zeroize::DefaultIsZeroes for G2Uncompressed {} + +impl AsRef<[u8]> for G2Uncompressed { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl AsMut<[u8]> for G2Uncompressed { + fn as_mut(&mut self) -> &mut [u8] { + &mut self.0 + } +} + +impl ConstantTimeEq for G2Uncompressed { + fn ct_eq(&self, other: &Self) -> Choice { + self.0.ct_eq(&other.0) + } +} + +impl Eq for G2Uncompressed {} +impl PartialEq for G2Uncompressed { + #[inline] + fn eq(&self, other: &Self) -> bool { + bool::from(self.ct_eq(other)) + } +} + +impl Group for G2Projective { + type Scalar = Scalar; + + fn random(mut rng: impl RngCore) -> Self { + loop { + let x = Fp2::random(&mut rng); + let flip_sign = rng.next_u32() % 2 != 0; + + // Obtain the corresponding y-coordinate given x as y = sqrt(x^3 + 4) + let p = ((x.square() * x) + B).sqrt().map(|y| G2Affine { + x, + y: if flip_sign { -y } else { y }, + infinity: 0.into(), + }); + + if p.is_some().into() { + let p = p.unwrap().to_curve().clear_cofactor(); + + if bool::from(!p.is_identity()) { + return p; + } + } + } + } + + fn identity() -> Self { + Self::identity() + } + + fn generator() -> Self { + Self::generator() + } + + fn is_identity(&self) -> Choice { + self.is_identity() + } + + #[must_use] + fn double(&self) -> Self { + self.double() + } +} + +#[cfg(feature = "alloc")] +impl WnafGroup for G2Projective { + fn recommended_wnaf_for_num_scalars(num_scalars: usize) -> usize { + const RECOMMENDATIONS: [usize; 11] = [1, 3, 8, 20, 47, 126, 260, 826, 1501, 4555, 84071]; + + let mut ret = 4; + for r in &RECOMMENDATIONS { + if num_scalars > *r { + ret += 1; + } else { + break; + } + } + + ret + } +} + +impl PrimeGroup for G2Projective {} + +impl Curve for G2Projective { + type AffineRepr = G2Affine; + + fn batch_normalize(p: &[Self], q: &mut [Self::AffineRepr]) { + Self::batch_normalize(p, q); + } + + fn to_affine(&self) -> Self::AffineRepr { + self.into() + } +} + +impl PrimeCurve for G2Projective { + type Affine = G2Affine; +} + +impl PrimeCurveAffine for G2Affine { + type Scalar = Scalar; + type Curve = G2Projective; + + fn identity() -> Self { + Self::identity() + } + + fn generator() -> Self { + Self::generator() + } + + fn is_identity(&self) -> Choice { + self.is_identity() + } + + fn to_curve(&self) -> Self::Curve { + self.into() + } +} + +impl GroupEncoding for G2Projective { + type Repr = G2Compressed; + + fn from_bytes(bytes: &Self::Repr) -> CtOption { + G2Affine::from_bytes(bytes).map(Self::from) + } + + fn from_bytes_unchecked(bytes: &Self::Repr) -> CtOption { + G2Affine::from_bytes_unchecked(bytes).map(Self::from) + } + + fn to_bytes(&self) -> Self::Repr { + G2Affine::from(self).to_bytes() + } +} + +impl GroupEncoding for G2Affine { + type Repr = G2Compressed; + + fn from_bytes(bytes: &Self::Repr) -> CtOption { + Self::from_compressed(&bytes.0) + } + + fn from_bytes_unchecked(bytes: &Self::Repr) -> CtOption { + Self::from_compressed_unchecked(&bytes.0) + } + + fn to_bytes(&self) -> Self::Repr { + G2Compressed(self.to_compressed()) + } +} + +impl UncompressedEncoding for G2Affine { + type Uncompressed = G2Uncompressed; + + fn from_uncompressed(bytes: &Self::Uncompressed) -> CtOption { + Self::from_uncompressed(&bytes.0) + } + + fn from_uncompressed_unchecked(bytes: &Self::Uncompressed) -> CtOption { + Self::from_uncompressed_unchecked(&bytes.0) + } + + fn to_uncompressed(&self) -> Self::Uncompressed { + G2Uncompressed(self.to_uncompressed()) + } +} + +#[test] +fn test_is_on_curve() { + assert!(bool::from(G2Affine::identity().is_on_curve())); + assert!(bool::from(G2Affine::generator().is_on_curve())); + assert!(bool::from(G2Projective::identity().is_on_curve())); + assert!(bool::from(G2Projective::generator().is_on_curve())); + + let z = Fp2 { + c0: Fp::from_raw_unchecked([ + 0xba7a_fa1f_9a6f_e250, + 0xfa0f_5b59_5eaf_e731, + 0x3bdc_4776_94c3_06e7, + 0x2149_be4b_3949_fa24, + 0x64aa_6e06_49b2_078c, + 0x12b1_08ac_3364_3c3e, + ]), + c1: Fp::from_raw_unchecked([ + 0x1253_25df_3d35_b5a8, + 0xdc46_9ef5_555d_7fe3, + 0x02d7_16d2_4431_06a9, + 0x05a1_db59_a6ff_37d0, + 0x7cf7_784e_5300_bb8f, + 0x16a8_8922_c7a5_e844, + ]), + }; + + let gen = G2Affine::generator(); + let mut test = G2Projective { + x: gen.x * z, + y: gen.y * z, + z, + }; + + assert!(bool::from(test.is_on_curve())); + + test.x = z; + assert!(!bool::from(test.is_on_curve())); +} + +#[test] +#[allow(clippy::eq_op)] +fn test_affine_point_equality() { + let a = G2Affine::generator(); + let b = G2Affine::identity(); + + assert!(a == a); + assert!(b == b); + assert!(a != b); + assert!(b != a); +} + +#[test] +#[allow(clippy::eq_op)] +fn test_projective_point_equality() { + let a = G2Projective::generator(); + let b = G2Projective::identity(); + + assert!(a == a); + assert!(b == b); + assert!(a != b); + assert!(b != a); + + let z = Fp2 { + c0: Fp::from_raw_unchecked([ + 0xba7a_fa1f_9a6f_e250, + 0xfa0f_5b59_5eaf_e731, + 0x3bdc_4776_94c3_06e7, + 0x2149_be4b_3949_fa24, + 0x64aa_6e06_49b2_078c, + 0x12b1_08ac_3364_3c3e, + ]), + c1: Fp::from_raw_unchecked([ + 0x1253_25df_3d35_b5a8, + 0xdc46_9ef5_555d_7fe3, + 0x02d7_16d2_4431_06a9, + 0x05a1_db59_a6ff_37d0, + 0x7cf7_784e_5300_bb8f, + 0x16a8_8922_c7a5_e844, + ]), + }; + + let mut c = G2Projective { + x: a.x * z, + y: a.y * z, + z, + }; + assert!(bool::from(c.is_on_curve())); + + assert!(a == c); + assert!(b != c); + assert!(c == a); + assert!(c != b); + + c.y = -c.y; + assert!(bool::from(c.is_on_curve())); + + assert!(a != c); + assert!(b != c); + assert!(c != a); + assert!(c != b); + + c.y = -c.y; + c.x = z; + assert!(!bool::from(c.is_on_curve())); + assert!(a != b); + assert!(a != c); + assert!(b != c); +} + +#[test] +fn test_conditionally_select_affine() { + let a = G2Affine::generator(); + let b = G2Affine::identity(); + + assert_eq!(G2Affine::conditional_select(&a, &b, Choice::from(0u8)), a); + assert_eq!(G2Affine::conditional_select(&a, &b, Choice::from(1u8)), b); +} + +#[test] +fn test_conditionally_select_projective() { + let a = G2Projective::generator(); + let b = G2Projective::identity(); + + assert_eq!( + G2Projective::conditional_select(&a, &b, Choice::from(0u8)), + a + ); + assert_eq!( + G2Projective::conditional_select(&a, &b, Choice::from(1u8)), + b + ); +} + +#[test] +fn test_projective_to_affine() { + let a = G2Projective::generator(); + let b = G2Projective::identity(); + + assert!(bool::from(G2Affine::from(a).is_on_curve())); + assert!(!bool::from(G2Affine::from(a).is_identity())); + assert!(bool::from(G2Affine::from(b).is_on_curve())); + assert!(bool::from(G2Affine::from(b).is_identity())); + + let z = Fp2 { + c0: Fp::from_raw_unchecked([ + 0xba7a_fa1f_9a6f_e250, + 0xfa0f_5b59_5eaf_e731, + 0x3bdc_4776_94c3_06e7, + 0x2149_be4b_3949_fa24, + 0x64aa_6e06_49b2_078c, + 0x12b1_08ac_3364_3c3e, + ]), + c1: Fp::from_raw_unchecked([ + 0x1253_25df_3d35_b5a8, + 0xdc46_9ef5_555d_7fe3, + 0x02d7_16d2_4431_06a9, + 0x05a1_db59_a6ff_37d0, + 0x7cf7_784e_5300_bb8f, + 0x16a8_8922_c7a5_e844, + ]), + }; + + let c = G2Projective { + x: a.x * z, + y: a.y * z, + z, + }; + + assert_eq!(G2Affine::from(c), G2Affine::generator()); +} + +#[test] +fn test_affine_to_projective() { + let a = G2Affine::generator(); + let b = G2Affine::identity(); + + assert!(bool::from(G2Projective::from(a).is_on_curve())); + assert!(!bool::from(G2Projective::from(a).is_identity())); + assert!(bool::from(G2Projective::from(b).is_on_curve())); + assert!(bool::from(G2Projective::from(b).is_identity())); +} + +#[test] +fn test_doubling() { + { + let tmp = G2Projective::identity().double(); + assert!(bool::from(tmp.is_identity())); + assert!(bool::from(tmp.is_on_curve())); + } + { + let tmp = G2Projective::generator().double(); + assert!(!bool::from(tmp.is_identity())); + assert!(bool::from(tmp.is_on_curve())); + + assert_eq!( + G2Affine::from(tmp), + G2Affine { + x: Fp2 { + c0: Fp::from_raw_unchecked([ + 0xe9d9_e2da_9620_f98b, + 0x54f1_1993_46b9_7f36, + 0x3db3_b820_376b_ed27, + 0xcfdb_31c9_b0b6_4f4c, + 0x41d7_c127_8635_4493, + 0x0571_0794_c255_c064, + ]), + c1: Fp::from_raw_unchecked([ + 0xd6c1_d3ca_6ea0_d06e, + 0xda0c_bd90_5595_489f, + 0x4f53_52d4_3479_221d, + 0x8ade_5d73_6f8c_97e0, + 0x48cc_8433_925e_f70e, + 0x08d7_ea71_ea91_ef81, + ]), + }, + y: Fp2 { + c0: Fp::from_raw_unchecked([ + 0x15ba_26eb_4b0d_186f, + 0x0d08_6d64_b7e9_e01e, + 0xc8b8_48dd_652f_4c78, + 0xeecf_46a6_123b_ae4f, + 0x255e_8dd8_b6dc_812a, + 0x1641_42af_21dc_f93f, + ]), + c1: Fp::from_raw_unchecked([ + 0xf9b4_a1a8_9598_4db4, + 0xd417_b114_cccf_f748, + 0x6856_301f_c89f_086e, + 0x41c7_7787_8931_e3da, + 0x3556_b155_066a_2105, + 0x00ac_f7d3_25cb_89cf, + ]), + }, + infinity: Choice::from(0u8) + } + ); + } +} + +#[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([ + 0xba7a_fa1f_9a6f_e250, + 0xfa0f_5b59_5eaf_e731, + 0x3bdc_4776_94c3_06e7, + 0x2149_be4b_3949_fa24, + 0x64aa_6e06_49b2_078c, + 0x12b1_08ac_3364_3c3e, + ]), + c1: Fp::from_raw_unchecked([ + 0x1253_25df_3d35_b5a8, + 0xdc46_9ef5_555d_7fe3, + 0x02d7_16d2_4431_06a9, + 0x05a1_db59_a6ff_37d0, + 0x7cf7_784e_5300_bb8f, + 0x16a8_8922_c7a5_e844, + ]), + }; + + b = G2Projective { + x: b.x * z, + y: b.y * 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([ + 0xba7a_fa1f_9a6f_e250, + 0xfa0f_5b59_5eaf_e731, + 0x3bdc_4776_94c3_06e7, + 0x2149_be4b_3949_fa24, + 0x64aa_6e06_49b2_078c, + 0x12b1_08ac_3364_3c3e, + ]), + c1: Fp::from_raw_unchecked([ + 0x1253_25df_3d35_b5a8, + 0xdc46_9ef5_555d_7fe3, + 0x02d7_16d2_4431_06a9, + 0x05a1_db59_a6ff_37d0, + 0x7cf7_784e_5300_bb8f, + 0x16a8_8922_c7a5_e844, + ]), + }; + + b = G2Projective { + x: b.x * z, + y: b.y * 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 += 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([ + 0xcd03_c9e4_8671_f071, + 0x5dab_2246_1fcd_a5d2, + 0x5870_42af_d385_1b95, + 0x8eb6_0ebe_01ba_cb9e, + 0x03f9_7d6e_83d0_50d2, + 0x18f0_2065_5463_8741, + ]), + 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([ + 0x705a_bc79_9ca7_73d3, + 0xfe13_2292_c1d4_bf08, + 0xf37e_ce3e_07b2_b466, + 0x887e_1c43_f447_e301, + 0x1e09_70d0_33bc_77e8, + 0x1985_c81e_20a6_93f2, + ]), + c1: Fp::from_raw_unchecked([ + 0x1d79_b25d_b36a_b924, + 0x2394_8e4d_5296_39d3, + 0x471b_a7fb_0d00_6297, + 0x2c36_d4b4_465d_c4c0, + 0x82bb_c3cf_ec67_f538, + 0x051d_2728_b67b_f952, + ]) + }, + y: Fp2 { + c0: Fp::from_raw_unchecked([ + 0x41b1_bbf6_576c_0abf, + 0xb6cc_9371_3f7a_0f9a, + 0x6b65_b43e_48f3_f01f, + 0xfb7a_4cfc_af81_be4f, + 0x3e32_dadc_6ec2_2cb6, + 0x0bb0_fc49_d798_07e3, + ]), + c1: Fp::from_raw_unchecked([ + 0x7d13_9778_8f5f_2ddf, + 0xab29_0714_4ff0_d8e8, + 0x5b75_73e0_cdb9_1f92, + 0x4cb8_932d_d31d_af28, + 0x62bb_fac6_db05_2a54, + 0x11f9_5c16_d14c_3bbe, + ]) + }, + z: Fp2::one() + }) + ); + assert!(!bool::from(c.is_identity())); + assert!(bool::from(c.is_on_curve())); + } +} + +#[test] +fn test_mixed_addition() { + { + let a = G2Affine::identity(); + let b = G2Projective::identity(); + let c = a + b; + assert!(bool::from(c.is_identity())); + assert!(bool::from(c.is_on_curve())); + } + { + let a = G2Affine::identity(); + let mut b = G2Projective::generator(); + { + let z = Fp2 { + c0: Fp::from_raw_unchecked([ + 0xba7a_fa1f_9a6f_e250, + 0xfa0f_5b59_5eaf_e731, + 0x3bdc_4776_94c3_06e7, + 0x2149_be4b_3949_fa24, + 0x64aa_6e06_49b2_078c, + 0x12b1_08ac_3364_3c3e, + ]), + c1: Fp::from_raw_unchecked([ + 0x1253_25df_3d35_b5a8, + 0xdc46_9ef5_555d_7fe3, + 0x02d7_16d2_4431_06a9, + 0x05a1_db59_a6ff_37d0, + 0x7cf7_784e_5300_bb8f, + 0x16a8_8922_c7a5_e844, + ]), + }; + + b = G2Projective { + x: b.x * z, + y: b.y * 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 = G2Affine::identity(); + let mut b = G2Projective::generator(); + { + let z = Fp2 { + c0: Fp::from_raw_unchecked([ + 0xba7a_fa1f_9a6f_e250, + 0xfa0f_5b59_5eaf_e731, + 0x3bdc_4776_94c3_06e7, + 0x2149_be4b_3949_fa24, + 0x64aa_6e06_49b2_078c, + 0x12b1_08ac_3364_3c3e, + ]), + c1: Fp::from_raw_unchecked([ + 0x1253_25df_3d35_b5a8, + 0xdc46_9ef5_555d_7fe3, + 0x02d7_16d2_4431_06a9, + 0x05a1_db59_a6ff_37d0, + 0x7cf7_784e_5300_bb8f, + 0x16a8_8922_c7a5_e844, + ]), + }; + + b = G2Projective { + x: b.x * z, + y: b.y * 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 += G2Affine::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([ + 0xcd03_c9e4_8671_f071, + 0x5dab_2246_1fcd_a5d2, + 0x5870_42af_d385_1b95, + 0x8eb6_0ebe_01ba_cb9e, + 0x03f9_7d6e_83d0_50d2, + 0x18f0_2065_5463_8741, + ]), + 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, + }; + let a = G2Affine::from(a); + 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([ + 0x705a_bc79_9ca7_73d3, + 0xfe13_2292_c1d4_bf08, + 0xf37e_ce3e_07b2_b466, + 0x887e_1c43_f447_e301, + 0x1e09_70d0_33bc_77e8, + 0x1985_c81e_20a6_93f2, + ]), + c1: Fp::from_raw_unchecked([ + 0x1d79_b25d_b36a_b924, + 0x2394_8e4d_5296_39d3, + 0x471b_a7fb_0d00_6297, + 0x2c36_d4b4_465d_c4c0, + 0x82bb_c3cf_ec67_f538, + 0x051d_2728_b67b_f952, + ]) + }, + y: Fp2 { + c0: Fp::from_raw_unchecked([ + 0x41b1_bbf6_576c_0abf, + 0xb6cc_9371_3f7a_0f9a, + 0x6b65_b43e_48f3_f01f, + 0xfb7a_4cfc_af81_be4f, + 0x3e32_dadc_6ec2_2cb6, + 0x0bb0_fc49_d798_07e3, + ]), + c1: Fp::from_raw_unchecked([ + 0x7d13_9778_8f5f_2ddf, + 0xab29_0714_4ff0_d8e8, + 0x5b75_73e0_cdb9_1f92, + 0x4cb8_932d_d31d_af28, + 0x62bb_fac6_db05_2a54, + 0x11f9_5c16_d14c_3bbe, + ]) + }, + z: Fp2::one() + }) + ); + assert!(!bool::from(c.is_identity())); + assert!(bool::from(c.is_on_curve())); + } +} + +#[test] +#[allow(clippy::eq_op)] +fn test_projective_negation_and_subtraction() { + let a = G2Projective::generator().double(); + assert_eq!(a + (-a), G2Projective::identity()); + assert_eq!(a + (-a), a - a); +} + +#[test] +fn test_affine_negation_and_subtraction() { + let a = G2Affine::generator(); + assert_eq!(G2Projective::from(a) + (-a), G2Projective::identity()); + assert_eq!(G2Projective::from(a) + (-a), G2Projective::from(a) - a); +} + +#[test] +fn test_projective_scalar_multiplication() { + let g = G2Projective::generator(); + let a = Scalar::from_raw([ + 0x2b56_8297_a56d_a71c, + 0xd8c3_9ecb_0ef3_75d1, + 0x435c_38da_67bf_bf96, + 0x8088_a050_26b6_59b2, + ]); + let b = Scalar::from_raw([ + 0x785f_dd9b_26ef_8b85, + 0xc997_f258_3769_5c18, + 0x4c8d_bc39_e7b7_56c1, + 0x70d9_b6cc_6d87_df20, + ]); + let c = a * b; + + assert_eq!((g * a) * b, g * c); +} + +#[test] +fn test_affine_scalar_multiplication() { + let g = G2Affine::generator(); + let a = Scalar::from_raw([ + 0x2b56_8297_a56d_a71c, + 0xd8c3_9ecb_0ef3_75d1, + 0x435c_38da_67bf_bf96, + 0x8088_a050_26b6_59b2, + ]); + let b = Scalar::from_raw([ + 0x785f_dd9b_26ef_8b85, + 0xc997_f258_3769_5c18, + 0x4c8d_bc39_e7b7_56c1, + 0x70d9_b6cc_6d87_df20, + ]); + let c = a * b; + + assert_eq!(G2Affine::from(g * a) * b, g * c); +} + +#[test] +fn test_is_torsion_free() { + let a = G2Affine { + x: Fp2 { + c0: Fp::from_raw_unchecked([ + 0x89f5_50c8_13db_6431, + 0xa50b_e8c4_56cd_8a1a, + 0xa45b_3741_14ca_e851, + 0xbb61_90f5_bf7f_ff63, + 0x970c_a02c_3ba8_0bc7, + 0x02b8_5d24_e840_fbac, + ]), + c1: Fp::from_raw_unchecked([ + 0x6888_bc53_d707_16dc, + 0x3dea_6b41_1768_2d70, + 0xd8f5_f930_500c_a354, + 0x6b5e_cb65_56f5_c155, + 0xc96b_ef04_3477_8ab0, + 0x0508_1505_5150_06ad, + ]), + }, + y: Fp2 { + c0: Fp::from_raw_unchecked([ + 0x3cf1_ea0d_434b_0f40, + 0x1a0d_c610_e603_e333, + 0x7f89_9561_60c7_2fa0, + 0x25ee_03de_cf64_31c5, + 0xeee8_e206_ec0f_e137, + 0x0975_92b2_26df_ef28, + ]), + c1: Fp::from_raw_unchecked([ + 0x71e8_bb5f_2924_7367, + 0xa5fe_049e_2118_31ce, + 0x0ce6_b354_502a_3896, + 0x93b0_1200_0997_314e, + 0x6759_f3b6_aa5b_42ac, + 0x1569_44c4_dfe9_2bbb, + ]), + }, + infinity: Choice::from(0u8), + }; + assert!(!bool::from(a.is_torsion_free())); + + assert!(bool::from(G2Affine::identity().is_torsion_free())); + assert!(bool::from(G2Affine::generator().is_torsion_free())); +} + +#[test] +fn test_mul_by_x() { + // multiplying by `x` a point in G2 is the same as multiplying by + // the equivalent scalar. + let generator = G2Projective::generator(); + let x = if crate::BLS_X_IS_NEGATIVE { + -Scalar::from(crate::BLS_X) + } else { + Scalar::from(crate::BLS_X) + }; + assert_eq!(generator.mul_by_x(), generator * x); + + let point = G2Projective::generator() * Scalar::from(42); + assert_eq!(point.mul_by_x(), point * x); +} + +#[test] +fn test_psi() { + let generator = G2Projective::generator(); + + let z = Fp2 { + c0: Fp::from_raw_unchecked([ + 0x0ef2ddffab187c0a, + 0x2424522b7d5ecbfc, + 0xc6f341a3398054f4, + 0x5523ddf409502df0, + 0xd55c0b5a88e0dd97, + 0x066428d704923e52, + ]), + c1: Fp::from_raw_unchecked([ + 0x538bbe0c95b4878d, + 0xad04a50379522881, + 0x6d5c05bf5c12fb64, + 0x4ce4a069a2d34787, + 0x59ea6c8d0dffaeaf, + 0x0d42a083a75bd6f3, + ]), + }; + + // `point` is a random point in the curve + let point = G2Projective { + x: Fp2 { + c0: Fp::from_raw_unchecked([ + 0xee4c8cb7c047eaf2, + 0x44ca22eee036b604, + 0x33b3affb2aefe101, + 0x15d3e45bbafaeb02, + 0x7bfc2154cd7419a4, + 0x0a2d0c2b756e5edc, + ]), + c1: Fp::from_raw_unchecked([ + 0xfc224361029a8777, + 0x4cbf2baab8740924, + 0xc5008c6ec6592c89, + 0xecc2c57b472a9c2d, + 0x8613eafd9d81ffb1, + 0x10fe54daa2d3d495, + ]), + } * z, + y: Fp2 { + c0: Fp::from_raw_unchecked([ + 0x7de7edc43953b75c, + 0x58be1d2de35e87dc, + 0x5731d30b0e337b40, + 0xbe93b60cfeaae4c9, + 0x8b22c203764bedca, + 0x01616c8d1033b771, + ]), + c1: Fp::from_raw_unchecked([ + 0xea126fe476b5733b, + 0x85cee68b5dae1652, + 0x98247779f7272b04, + 0xa649c8b468c6e808, + 0xb5b9a62dff0c4e45, + 0x1555b67fc7bbe73d, + ]), + }, + z: z.square() * z, + }; + assert!(bool::from(point.is_on_curve())); + + // psi2(P) = psi(psi(P)) + assert_eq!(generator.psi2(), generator.psi().psi()); + assert_eq!(point.psi2(), point.psi().psi()); + // psi(P) is a morphism + assert_eq!(generator.double().psi(), generator.psi().double()); + assert_eq!(point.psi() + generator.psi(), (point + generator).psi()); + // psi(P) behaves in the same way on the same projective point + let mut normalized_point = [G2Affine::identity()]; + G2Projective::batch_normalize(&[point], &mut normalized_point); + let normalized_point = G2Projective::from(normalized_point[0]); + assert_eq!(point.psi(), normalized_point.psi()); + assert_eq!(point.psi2(), normalized_point.psi2()); +} + +#[test] +fn test_clear_cofactor() { + let z = Fp2 { + c0: Fp::from_raw_unchecked([ + 0x0ef2ddffab187c0a, + 0x2424522b7d5ecbfc, + 0xc6f341a3398054f4, + 0x5523ddf409502df0, + 0xd55c0b5a88e0dd97, + 0x066428d704923e52, + ]), + c1: Fp::from_raw_unchecked([ + 0x538bbe0c95b4878d, + 0xad04a50379522881, + 0x6d5c05bf5c12fb64, + 0x4ce4a069a2d34787, + 0x59ea6c8d0dffaeaf, + 0x0d42a083a75bd6f3, + ]), + }; + + // `point` is a random point in the curve + let point = G2Projective { + x: Fp2 { + c0: Fp::from_raw_unchecked([ + 0xee4c8cb7c047eaf2, + 0x44ca22eee036b604, + 0x33b3affb2aefe101, + 0x15d3e45bbafaeb02, + 0x7bfc2154cd7419a4, + 0x0a2d0c2b756e5edc, + ]), + c1: Fp::from_raw_unchecked([ + 0xfc224361029a8777, + 0x4cbf2baab8740924, + 0xc5008c6ec6592c89, + 0xecc2c57b472a9c2d, + 0x8613eafd9d81ffb1, + 0x10fe54daa2d3d495, + ]), + } * z, + y: Fp2 { + c0: Fp::from_raw_unchecked([ + 0x7de7edc43953b75c, + 0x58be1d2de35e87dc, + 0x5731d30b0e337b40, + 0xbe93b60cfeaae4c9, + 0x8b22c203764bedca, + 0x01616c8d1033b771, + ]), + c1: Fp::from_raw_unchecked([ + 0xea126fe476b5733b, + 0x85cee68b5dae1652, + 0x98247779f7272b04, + 0xa649c8b468c6e808, + 0xb5b9a62dff0c4e45, + 0x1555b67fc7bbe73d, + ]), + }, + z: z.square() * z, + }; + + assert!(bool::from(point.is_on_curve())); + assert!(!bool::from(G2Affine::from(point).is_torsion_free())); + let cleared_point = point.clear_cofactor(); + + assert!(bool::from(cleared_point.is_on_curve())); + assert!(bool::from(G2Affine::from(cleared_point).is_torsion_free())); + + // the generator (and the identity) are always on the curve, + // even after clearing the cofactor + let generator = G2Projective::generator(); + assert!(bool::from(generator.clear_cofactor().is_on_curve())); + let id = G2Projective::identity(); + assert!(bool::from(id.clear_cofactor().is_on_curve())); + + // test the effect on q-torsion points multiplying by h_eff modulo |Scalar| + // h_eff % q = 0x2b116900400069009a40200040001ffff + let h_eff_modq: [u8; 32] = [ + 0xff, 0xff, 0x01, 0x00, 0x04, 0x00, 0x02, 0xa4, 0x09, 0x90, 0x06, 0x00, 0x04, 0x90, 0x16, + 0xb1, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, + ]; + assert_eq!(generator.clear_cofactor(), generator.multiply(&h_eff_modq)); + assert_eq!( + cleared_point.clear_cofactor(), + cleared_point.multiply(&h_eff_modq) + ); +} + +#[test] +fn test_batch_normalize() { + let a = G2Projective::generator().double(); + let b = a.double(); + let c = b.double(); + + for a_identity in (0..=1).map(|n| n == 1) { + for b_identity in (0..=1).map(|n| n == 1) { + for c_identity in (0..=1).map(|n| n == 1) { + let mut v = [a, b, c]; + if a_identity { + v[0] = G2Projective::identity() + } + if b_identity { + v[1] = G2Projective::identity() + } + if c_identity { + v[2] = G2Projective::identity() + } + + let mut t = [ + G2Affine::identity(), + G2Affine::identity(), + G2Affine::identity(), + ]; + let expected = [ + G2Affine::from(v[0]), + G2Affine::from(v[1]), + G2Affine::from(v[2]), + ]; + + G2Projective::batch_normalize(&v[..], &mut t[..]); + + assert_eq!(&t[..], &expected[..]); + } + } + } +} + +#[cfg(feature = "zeroize")] +#[test] +fn test_zeroize() { + use zeroize::Zeroize; + + let mut a = G2Affine::generator(); + a.zeroize(); + assert!(bool::from(a.is_identity())); + + let mut a = G2Projective::generator(); + a.zeroize(); + assert!(bool::from(a.is_identity())); + + let mut a = GroupEncoding::to_bytes(&G2Affine::generator()); + a.zeroize(); + assert_eq!(&a, &G2Compressed::default()); + + let mut a = UncompressedEncoding::to_uncompressed(&G2Affine::generator()); + a.zeroize(); + assert_eq!(&a, &G2Uncompressed::default()); +} + +#[test] +fn test_commutative_scalar_subgroup_multiplication() { + let a = Scalar::from_raw([ + 0x1fff_3231_233f_fffd, + 0x4884_b7fa_0003_4802, + 0x998c_4fef_ecbc_4ff3, + 0x1824_b159_acc5_0562, + ]); + + let g2_a = G2Affine::generator(); + let g2_p = G2Projective::generator(); + + // By reference. + assert_eq!(&g2_a * &a, &a * &g2_a); + assert_eq!(&g2_p * &a, &a * &g2_p); + + // Mixed + assert_eq!(&g2_a * a.clone(), a.clone() * &g2_a); + assert_eq!(&g2_p * a.clone(), a.clone() * &g2_p); + assert_eq!(g2_a.clone() * &a, &a * g2_a.clone()); + assert_eq!(g2_p.clone() * &a, &a * g2_p.clone()); + + // By value. + assert_eq!(g2_p * a, a * g2_p); + assert_eq!(g2_a * a, a * g2_a); +} diff --git a/constantine/bls12_381/src/hash_to_curve/chain.rs b/constantine/bls12_381/src/hash_to_curve/chain.rs new file mode 100644 index 000000000..c9d2352f2 --- /dev/null +++ b/constantine/bls12_381/src/hash_to_curve/chain.rs @@ -0,0 +1,920 @@ +//! Addition chains for computing square roots. +//! chain_pm3div4: input x, output x^((p-3)//4). +//! chain_p2m9div16: input x, output x^((p**2 - 9) // 16). + +use core::ops::MulAssign; + +use crate::{fp::Fp, fp2::Fp2}; + +macro_rules! square { + ($var:expr, $n:expr) => { + for _ in 0..$n { + $var = $var.square(); + } + }; +} + +#[allow(clippy::cognitive_complexity)] +/// addchain for 1000602388805416848354447456433976039139220704984751971333014534031007912622709466110671907282253916009473568139946 +/// Bos-Coster (win=4) : 458 links, 16 variables */ +/// Addition chain implementing exponentiation by (p - 3) // 4. +pub fn chain_pm3div4(var0: &Fp) -> Fp { + let mut var1 = var0.square(); + //Self::sqr(var1, var0); /* 0 : 2 */ + let var9 = var1 * var0; + //Self::mul(&mut var9, var1, var0); /* 1 : 3 */ + let var5 = var1.square(); + //Self::sqr(&mut var5, var1); /* 2 : 4 */ + let var2 = var9 * var1; + //Self::mul(&mut var2, &var9, var1); /* 3 : 5 */ + let var7 = var5 * var9; + //Self::mul(&mut var7, &var5, &var9); /* 4 : 7 */ + let var10 = var2 * var5; + //Self::mul(&mut var10, &var2, &var5); /* 5 : 9 */ + let var13 = var7 * var5; + //Self::mul(&mut var13, &var7, &var5); /* 6 : 11 */ + let var4 = var10 * var5; + //Self::mul(&mut var4, &var10, &var5); /* 7 : 13 */ + let var8 = var13 * var5; + //Self::mul(&mut var8, &var13, &var5); /* 8 : 15 */ + let var15 = var4 * var5; + //Self::mul(&mut var15, &var4, &var5); /* 9 : 17 */ + let var11 = var8 * var5; + //Self::mul(&mut var11, &var8, &var5); /* 10 : 19 */ + let var3 = var15 * var5; + //Self::mul(&mut var3, &var15, &var5); /* 11 : 21 */ + let var12 = var11 * var5; + //Self::mul(&mut var12, &var11, &var5); /* 12 : 23 */ + var1 = var4.square(); + //Self::sqr(var1, &var4); /* 13 : 26 */ + let var14 = var12 * var5; + //Self::mul(&mut var14, &var12, &var5); /* 14 : 27 */ + let var6 = var1 * var9; + //Self::mul(&mut var6, var1, &var9); /* 15 : 29 */ + let var5 = var1 * var2; + //Self::mul(&mut var5, var1, &var2); /* 16 : 31 */ + // 17 : 106496 + square!(var1, 12); + // 29 : 106513 + var1.mul_assign(&var15); + // 30 : 13633664 + square!(var1, 7); + // 37 : 13633679 + var1.mul_assign(&var8); + // 38 : 218138864 + square!(var1, 4); + // 42 : 218138869 + var1.mul_assign(&var2); + // 43 : 13960887616 + square!(var1, 6); + // 49 : 13960887623 + var1.mul_assign(&var7); + // 50 : 1786993615744 + square!(var1, 7); + // 57 : 1786993615767 + var1.mul_assign(&var12); + // 58 : 57183795704544 + square!(var1, 5); + // 63 : 57183795704575 + var1.mul_assign(&var5); + // 64 : 228735182818300 + square!(var1, 2); + // 66 : 228735182818303 + var1.mul_assign(&var9); + // 67 : 14639051700371392 + square!(var1, 6); + // 73 : 14639051700371405 + var1.mul_assign(&var4); + // 74 : 936899308823769920 + square!(var1, 6); + // 80 : 936899308823769933 + var1.mul_assign(&var4); + // 81 : 59961555764721275712 + square!(var1, 6); + // 87 : 59961555764721275721 + var1.mul_assign(&var10); + // 88 : 479692446117770205768 + square!(var1, 3); + // 91 : 479692446117770205771 + var1.mul_assign(&var9); + // 92 : 61400633103074586338688 + square!(var1, 7); + // 99 : 61400633103074586338701 + var1.mul_assign(&var4); + // 100 : 982410129649193381419216 + square!(var1, 4); + // 104 : 982410129649193381419229 + var1.mul_assign(&var4); + // 105 : 62874248297548376410830656 + square!(var1, 6); + // 111 : 62874248297548376410830671 + var1.mul_assign(&var8); + // 112 : 4023951891043096090293162944 + square!(var1, 6); + // 118 : 4023951891043096090293162971 + var1.mul_assign(&var14); + // 119 : 32191615128344768722345303768 + square!(var1, 3); + // 122 : 32191615128344768722345303769 + var1.mul_assign(var0); + // 123 : 8241053472856260792920397764864 + square!(var1, 8); + // 131 : 8241053472856260792920397764877 + var1.mul_assign(&var4); + // 132 : 1054854844525601381493810913904256 + square!(var1, 7); + // 139 : 1054854844525601381493810913904279 + var1.mul_assign(&var12); + // 140 : 33755355024819244207801949244936928 + square!(var1, 5); + // 145 : 33755355024819244207801949244936939 + var1.mul_assign(&var13); + // 146 : 2160342721588431629299324751675964096 + square!(var1, 6); + // 152 : 2160342721588431629299324751675964109 + var1.mul_assign(&var4); + // 153 : 138261934181659624275156784107261702976 + square!(var1, 6); + // 159 : 138261934181659624275156784107261703005 + var1.mul_assign(&var6); + // 160 : 2212190946906553988402508545716187248080 + square!(var1, 4); + // 164 : 2212190946906553988402508545716187248089 + var1.mul_assign(&var10); + // 165 : 566320882408077821031042187703343935510784 + square!(var1, 8); + // 173 : 566320882408077821031042187703343935510813 + var1.mul_assign(&var6); + // 174 : 9061134118529245136496675003253502968173008 + square!(var1, 4); + // 178 : 9061134118529245136496675003253502968173021 + var1.mul_assign(&var4); + // 179 : 1159825167171743377471574400416448379926146688 + square!(var1, 7); + // 186 : 1159825167171743377471574400416448379926146711 + var1.mul_assign(&var12); + // 187 : 593830485591932609265446093013221570522187116032 + square!(var1, 9); + // 196 : 593830485591932609265446093013221570522187116051 + var1.mul_assign(&var11); + // 197 : 2375321942367730437061784372052886282088748464204 + square!(var1, 2); + // 199 : 2375321942367730437061784372052886282088748464207 + var1.mul_assign(&var9); + // 200 : 76010302155767373985977099905692361026839950854624 + square!(var1, 5); + // 205 : 76010302155767373985977099905692361026839950854631 + var1.mul_assign(&var7); + // 206 : 9729318675938223870205068787928622211435513709392768 + square!(var1, 7); + // 213 : 9729318675938223870205068787928622211435513709392773 + var1.mul_assign(&var2); + // 214 : 1245352790520092655386248804854863643063745754802274944 + square!(var1, 7); + // 221 : 1245352790520092655386248804854863643063745754802274953 + var1.mul_assign(&var10); + // 222 : 79702578593285929944719923510711273156079728307345596992 + square!(var1, 6); + // 228 : 79702578593285929944719923510711273156079728307345597015 + var1.mul_assign(&var12); + // 229 : 2550482514985149758231037552342760740994551305835059104480 + square!(var1, 5); + // 234 : 2550482514985149758231037552342760740994551305835059104509 + var1.mul_assign(&var6); + // 235 : 81615440479524792263393201674968343711825641786721891344288 + square!(var1, 5); + // 240 : 81615440479524792263393201674968343711825641786721891344307 + var1.mul_assign(&var11); + // 241 : 2611694095344793352428582453598986998778420537175100523017824 + square!(var1, 5); + // 246 : 2611694095344793352428582453598986998778420537175100523017843 + var1.mul_assign(&var11); + // 247 : 668593688408267098221717108121340671687275657516825733892567808 + square!(var1, 8); + // 255 : 668593688408267098221717108121340671687275657516825733892567821 + var1.mul_assign(&var4); + // 256 : 85579992116258188572379789839531605975971284162153693938248681088 + square!(var1, 7); + // 263 : 85579992116258188572379789839531605975971284162153693938248681109 + var1.mul_assign(&var3); + // 264 : 43816955963524192549058452397840182259697297491022691296383324727808 + square!(var1, 9); + // 273 : 43816955963524192549058452397840182259697297491022691296383324727823 + var1.mul_assign(&var8); + // 274 : 1402142590832774161569870476730885832310313519712726121484266391290336 + square!(var1, 5); + // 279 : 1402142590832774161569870476730885832310313519712726121484266391290349 + var1.mul_assign(&var4); + // 280 : 11217140726662193292558963813847086658482508157701808971874131130322792 + square!(var1, 3); + // 283 : 11217140726662193292558963813847086658482508157701808971874131130322795 + var1.mul_assign(&var9); + // 284 : 2871588026025521482895094736344854184571522088371663096799777569362635520 + square!(var1, 8); + // 292 : 2871588026025521482895094736344854184571522088371663096799777569362635535 + var1.mul_assign(&var8); + // 293 : 22972704208204171863160757890758833476572176706973304774398220554901084280 + square!(var1, 3); + // 296 : 22972704208204171863160757890758833476572176706973304774398220554901084283 + var1.mul_assign(&var9); + // 297 : 2940506138650133998484577010017130685001238618492583011122972231027338788224 + square!(var1, 7); + // 304 : 2940506138650133998484577010017130685001238618492583011122972231027338788233 + var1.mul_assign(&var10); + // 305 : 1505539142988868607224103429128770910720634172668202501694961782285997459575296 + square!(var1, 9); + // 314 : 1505539142988868607224103429128770910720634172668202501694961782285997459575311 + var1.mul_assign(&var8); + // 315 : 96354505151287590862342619464241338286120587050764960108477554066303837412819904 + square!(var1, 6); + // 321 : 96354505151287590862342619464241338286120587050764960108477554066303837412819925 + var1.mul_assign(&var3); + // 322 : 6166688329682405815189927645711445650311717571248957446942563460243445594420475200 + square!(var1, 6); + // 328 : 6166688329682405815189927645711445650311717571248957446942563460243445594420475231 + var1.mul_assign(&var5); + // 329 : 197334026549836986086077684662766260809974962279966638302162030727790259021455207392 + square!(var1, 5); + // 334 : 197334026549836986086077684662766260809974962279966638302162030727790259021455207423 + var1.mul_assign(&var5); + // 335 : 6314688849594783554754485909208520345919198792958932425669184983289288288686566637536 + square!(var1, 5); + // 340 : 6314688849594783554754485909208520345919198792958932425669184983289288288686566637567 + var1.mul_assign(&var5); + // 341 : 101035021593516536876071774547336325534707180687342918810706959732628612618985066201072 + square!(var1, 4); + // 345 : 101035021593516536876071774547336325534707180687342918810706959732628612618985066201085 + var1.mul_assign(&var4); + // 346 : 808280172748132295008574196378690604277657445498743350485655677861028900951880529608680 + square!(var1, 3); + // 349 : 808280172748132295008574196378690604277657445498743350485655677861028900951880529608683 + var1.mul_assign(&var9); + // 350 : 206919724223521867522194994272944794695080306047678297724327853532423398643681415579822848 + square!(var1, 8); + // 358 : 206919724223521867522194994272944794695080306047678297724327853532423398643681415579822869 + var1.mul_assign(&var3); + // 359 : 26485724700610799042840959266936933720970279174102822108713965252150195026391221194217327232 + square!(var1, 7); + // 366 : 26485724700610799042840959266936933720970279174102822108713965252150195026391221194217327263 + var1.mul_assign(&var5); + // 367 : 847543190419545569370910696541981879071048933571290307478846888068806240844519078214954472416 + square!(var1, 5); + // 372 : 847543190419545569370910696541981879071048933571290307478846888068806240844519078214954472447 + var1.mul_assign(&var5); + // 373 : 27121382093425458219869142289343420130273565874281289839323100418201799707024610502878543118304 + square!(var1, 5); + // 378 : 27121382093425458219869142289343420130273565874281289839323100418201799707024610502878543118335 + var1.mul_assign(&var5); + // 379 : 433942113494807331517906276629494722084377053988500637429169606691228795312393768046056689893360 + square!(var1, 4); + // 383 : 433942113494807331517906276629494722084377053988500637429169606691228795312393768046056689893375 + var1.mul_assign(&var8); + // 384 : 6943073815916917304286500426071915553350032863816010198866713707059660724998300288736907038294000 + square!(var1, 4); + // 388 : 6943073815916917304286500426071915553350032863816010198866713707059660724998300288736907038294007 + var1.mul_assign(&var7); + // 389 : 888713448437365414948672054537205190828804206568449305454939354503636572799782436958324100901632896 + square!(var1, 7); + // 396 : 888713448437365414948672054537205190828804206568449305454939354503636572799782436958324100901632927 + var1.mul_assign(&var5); + // 397 : 28438830349995693278357505745190566106521734610190377774558059344116370329593037982666371228852253664 + square!(var1, 5); + // 402 : 28438830349995693278357505745190566106521734610190377774558059344116370329593037982666371228852253693 + var1.mul_assign(&var6); + // 403 : 910042571199862184907440183846098115408695507526092088785857899011723850546977215445323879323272118176 + square!(var1, 5); + // 408 : 910042571199862184907440183846098115408695507526092088785857899011723850546977215445323879323272118207 + var1.mul_assign(&var5); + // 409 : 29121362278395589917038085883075139693078256240834946841147452768375163217503270894250364138344707782624 + square!(var1, 5); + // 414 : 29121362278395589917038085883075139693078256240834946841147452768375163217503270894250364138344707782655 + var1.mul_assign(&var5); + // 415 : 931883592908658877345218748258404470178504199706718298916718488588005222960104668616011652427030649044960 + square!(var1, 5); + // 420 : 931883592908658877345218748258404470178504199706718298916718488588005222960104668616011652427030649044991 + var1.mul_assign(&var5); + // 421 : 29820274973077084075046999944268943045712134390614985565334991634816167134723349395712372877664980769439712 + square!(var1, 5); + // 426 : 29820274973077084075046999944268943045712134390614985565334991634816167134723349395712372877664980769439743 + var1.mul_assign(&var5); + // 427 : 954248799138466690401503998216606177462788300499679538090719732314117348311147180662795932085279384622071776 + square!(var1, 5); + // 432 : 954248799138466690401503998216606177462788300499679538090719732314117348311147180662795932085279384622071807 + var1.mul_assign(&var5); + // 433 : 30535961572430934092848127942931397678809225615989745218903031434051755145956709781209469826728940307906297824 + square!(var1, 5); + // 438 : 30535961572430934092848127942931397678809225615989745218903031434051755145956709781209469826728940307906297855 + var1.mul_assign(&var5); + // 439 : 488575385158894945485570047086902362860947609855835923502448502944828082335307356499351517227663044926500765680 + square!(var1, 4); + // 443 : 488575385158894945485570047086902362860947609855835923502448502944828082335307356499351517227663044926500765693 + var1.mul_assign(&var4); + // 444 : 31268824650169276511076483013561751223100647030773499104156704188468997269459670815958497102570434875296049004352 + square!(var1, 6); + // 450 : 31268824650169276511076483013561751223100647030773499104156704188468997269459670815958497102570434875296049004373 + var1.mul_assign(&var3); + // 451 : 500301194402708424177223728216988019569610352492375985666507267015503956311354733055335953641126958004736784069968 + square!(var1, 4); + // 455 : 500301194402708424177223728216988019569610352492375985666507267015503956311354733055335953641126958004736784069973 + var1.mul_assign(&var2); + // 456 : 1000602388805416848354447456433976039139220704984751971333014534031007912622709466110671907282253916009473568139946 + var1.square() +} + +#[allow(clippy::cognitive_complexity)] +/// addchain for 1001205140483106588246484290269935788605945006208159541241399033561623546780709821462541004956387089373434649096260670658193992783731681621012512651314777238193313314641988297376025498093520728838658813979860931248214124593092835 +/// Bos-Coster (win=4) : 895 links, 17 variables +/// Addition chain implementing exponentiation by (p**2 - 9) // 16. +pub fn chain_p2m9div16(var0: &Fp2) -> Fp2 { + let mut var1 = var0.square(); + //Self::sqr(var1, var0); /* 0 : 2 */ + let var2 = var1 * var0; + //Self::mul(&mut var2, var1, var0); /* 1 : 3 */ + let var15 = var2 * var1; + //Self::mul(&mut var15, &var2, var1); /* 2 : 5 */ + let var3 = var15 * var1; + //Self::mul(&mut var3, &var15, var1); /* 3 : 7 */ + let var14 = var3 * var1; + //Self::mul(&mut var14, &var3, var1); /* 4 : 9 */ + let var13 = var14 * var1; + //Self::mul(&mut var13, &var14, var1); /* 5 : 11 */ + let var5 = var13 * var1; + //Self::mul(&mut var5, &var13, var1); /* 6 : 13 */ + let var10 = var5 * var1; + //Self::mul(&mut var10, &var5, var1); /* 7 : 15 */ + let var9 = var10 * var1; + //Self::mul(&mut var9, &var10, var1); /* 8 : 17 */ + let var16 = var9 * var1; + //Self::mul(&mut var16, &var9, var1); /* 9 : 19 */ + let var4 = var16 * var1; + //Self::mul(&mut var4, &var16, var1); /* 10 : 21 */ + let var7 = var4 * var1; + //Self::mul(&mut var7, &var4, var1); /* 11 : 23 */ + let var6 = var7 * var1; + //Self::mul(&mut var6, &var7, var1); /* 12 : 25 */ + let var12 = var6 * var1; + //Self::mul(&mut var12, &var6, var1); /* 13 : 27 */ + let var8 = var12 * var1; + //Self::mul(&mut var8, &var12, var1); /* 14 : 29 */ + let var11 = var8 * var1; + //Self::mul(&mut var11, &var8, var1); /* 15 : 31 */ + var1 = var4.square(); + //Self::sqr(var1, &var4); /* 16 : 42 */ + // 17 : 168 + square!(var1, 2); + // 19 : 169 + var1.mul_assign(var0); + // 20 : 86528 + square!(var1, 9); + // 29 : 86555 + var1.mul_assign(&var12); + // 30 : 1384880 + square!(var1, 4); + // 34 : 1384893 + var1.mul_assign(&var5); + // 35 : 88633152 + square!(var1, 6); + // 41 : 88633161 + var1.mul_assign(&var14); + // 42 : 1418130576 + square!(var1, 4); + // 46 : 1418130583 + var1.mul_assign(&var3); + // 47 : 45380178656 + square!(var1, 5); + // 52 : 45380178659 + var1.mul_assign(&var2); + // 53 : 11617325736704 + square!(var1, 8); + // 61 : 11617325736717 + var1.mul_assign(&var5); + // 62 : 185877211787472 + square!(var1, 4); + // 66 : 185877211787479 + var1.mul_assign(&var3); + // 67 : 2974035388599664 + square!(var1, 4); + // 71 : 2974035388599679 + var1.mul_assign(&var10); + // 72 : 761353059481517824 + square!(var1, 8); + // 80 : 761353059481517853 + var1.mul_assign(&var8); + // 81 : 48726595806817142592 + square!(var1, 6); + // 87 : 48726595806817142603 + var1.mul_assign(&var13); + // 88 : 779625532909074281648 + square!(var1, 4); + // 92 : 779625532909074281661 + var1.mul_assign(&var5); + // 93 : 6237004263272594253288 + square!(var1, 3); + // 96 : 6237004263272594253289 + var1.mul_assign(var0); + // 97 : 399168272849446032210496 + square!(var1, 6); + // 103 : 399168272849446032210511 + var1.mul_assign(&var10); + // 104 : 102187077849458184245890816 + square!(var1, 8); + // 112 : 102187077849458184245890845 + var1.mul_assign(&var8); + // 113 : 6539972982365323791737014080 + square!(var1, 6); + // 119 : 6539972982365323791737014101 + var1.mul_assign(&var4); + // 120 : 1674233083485522890684675609856 + square!(var1, 8); + // 128 : 1674233083485522890684675609873 + var1.mul_assign(&var9); + // 129 : 53575458671536732501909619515936 + square!(var1, 5); + // 134 : 53575458671536732501909619515951 + var1.mul_assign(&var10); + // 135 : 3428829354978350880122215649020864 + square!(var1, 6); + // 141 : 3428829354978350880122215649020873 + var1.mul_assign(&var14); + // 142 : 109722539359307228163910900768667936 + square!(var1, 5); + // 147 : 109722539359307228163910900768667951 + var1.mul_assign(&var10); + // 148 : 438890157437228912655643603074671804 + square!(var1, 2); + // 150 : 438890157437228912655643603074671805 + var1.mul_assign(var0); + // 151 : 28088970075982650409961190596778995520 + square!(var1, 6); + // 157 : 28088970075982650409961190596778995535 + var1.mul_assign(&var10); + // 158 : 3595388169725779252475032396387711428480 + square!(var1, 7); + // 165 : 3595388169725779252475032396387711428491 + var1.mul_assign(&var13); + // 166 : 57526210715612468039600518342203382855856 + square!(var1, 4); + // 170 : 57526210715612468039600518342203382855863 + var1.mul_assign(&var3); + // 171 : 3681677485799197954534433173901016502775232 + square!(var1, 6); + // 177 : 3681677485799197954534433173901016502775241 + var1.mul_assign(&var14); + // 178 : 471254718182297338180407446259330112355230848 + square!(var1, 7); + // 185 : 471254718182297338180407446259330112355230855 + var1.mul_assign(&var3); + // 186 : 15080150981833514821773038280298563595367387360 + square!(var1, 5); + // 191 : 15080150981833514821773038280298563595367387365 + var1.mul_assign(&var15); + // 192 : 1930259325674689897186948899878216140207025582720 + square!(var1, 7); + // 199 : 1930259325674689897186948899878216140207025582727 + var1.mul_assign(&var3); + // 200 : 61768298421590076709982364796102916486624818647264 + square!(var1, 5); + // 205 : 61768298421590076709982364796102916486624818647271 + var1.mul_assign(&var3); + // 206 : 63250737583708238551021941551209386482303814294805504 + square!(var1, 10); + // 216 : 63250737583708238551021941551209386482303814294805521 + var1.mul_assign(&var9); + // 217 : 506005900669665908408175532409675091858430514358444168 + square!(var1, 3); + // 220 : 506005900669665908408175532409675091858430514358444173 + var1.mul_assign(&var15); + // 221 : 16192188821429309069061617037109602939469776459470213536 + square!(var1, 5); + // 226 : 16192188821429309069061617037109602939469776459470213549 + var1.mul_assign(&var5); + // 227 : 4145200338285903121679773961500058352504262773624374668544 + square!(var1, 8); + // 235 : 4145200338285903121679773961500058352504262773624374668569 + var1.mul_assign(&var6); + // 236 : 132646410825148899893752766768001867280136408755979989394208 + square!(var1, 5); + // 241 : 132646410825148899893752766768001867280136408755979989394231 + var1.mul_assign(&var7); + // 242 : 8489370292809529593200177073152119505928730160382719321230784 + square!(var1, 6); + // 248 : 8489370292809529593200177073152119505928730160382719321230795 + var1.mul_assign(&var13); + // 249 : 543319698739809893964811332681735648379438730264494036558770880 + square!(var1, 6); + // 255 : 543319698739809893964811332681735648379438730264494036558770895 + var1.mul_assign(&var10); + // 256 : 34772460719347833213747925291631081496284078736927618339761337280 + square!(var1, 6); + // 262 : 34772460719347833213747925291631081496284078736927618339761337289 + var1.mul_assign(&var14); + // 263 : 4450874972076522651359734437328778431524362078326735147489451172992 + square!(var1, 7); + // 270 : 4450874972076522651359734437328778431524362078326735147489451173011 + var1.mul_assign(&var16); + // 271 : 142427999106448724843511501994520909808779586506455524719662437536352 + square!(var1, 5); + // 276 : 142427999106448724843511501994520909808779586506455524719662437536361 + var1.mul_assign(&var14); + // 277 : 9115391942812718389984736127649338227761893536413153582058396002327104 + square!(var1, 6); + // 283 : 9115391942812718389984736127649338227761893536413153582058396002327119 + var1.mul_assign(&var10); + // 284 : 583385084340013976959023112169557646576761186330441829251737344148935616 + square!(var1, 6); + // 290 : 583385084340013976959023112169557646576761186330441829251737344148935633 + var1.mul_assign(&var9); + // 291 : 18668322698880447262688739589425844690456357962574138536055595012765940256 + square!(var1, 5); + // 296 : 18668322698880447262688739589425844690456357962574138536055595012765940271 + var1.mul_assign(&var10); + // 297 : 74673290795521789050754958357703378761825431850296554144222380051063761084 + square!(var1, 2); + // 299 : 74673290795521789050754958357703378761825431850296554144222380051063761085 + var1.mul_assign(var0); + // 300 : 19116362443653577996993269339572064963027310553675917860920929293072322837760 + square!(var1, 8); + // 308 : 19116362443653577996993269339572064963027310553675917860920929293072322837765 + var1.mul_assign(&var15); + // 309 : 2446894392787657983615138475465224315267495750870517486197878949513257323233920 + square!(var1, 7); + // 316 : 2446894392787657983615138475465224315267495750870517486197878949513257323233925 + var1.mul_assign(&var15); + // 317 : 39150310284602527737842215607443589044279932013928279779166063192212117171742800 + square!(var1, 4); + // 321 : 39150310284602527737842215607443589044279932013928279779166063192212117171742803 + var1.mul_assign(&var2); + // 322 : 5011239716429123550443803597752779397667831297782819811733256088603150997983078784 + square!(var1, 7); + // 329 : 5011239716429123550443803597752779397667831297782819811733256088603150997983078795 + var1.mul_assign(&var13); + // 330 : 320719341851463907228403430256177881450741203058100467950928389670601663870917042880 + square!(var1, 6); + // 336 : 320719341851463907228403430256177881450741203058100467950928389670601663870917042895 + var1.mul_assign(&var10); + // 337 : 5131509469623422515654454884098846103211859248929607487214854234729626621934672686320 + square!(var1, 4); + // 341 : 5131509469623422515654454884098846103211859248929607487214854234729626621934672686333 + var1.mul_assign(&var5); + // 342 : 656833212111798082003770225164652301211117983862989758363501342045392207607638103850624 + square!(var1, 7); + // 349 : 656833212111798082003770225164652301211117983862989758363501342045392207607638103850635 + var1.mul_assign(&var13); + // 350 : 42037325575155077248241294410537747277511550967231344535264085890905101286888838646440640 + square!(var1, 6); + // 356 : 42037325575155077248241294410537747277511550967231344535264085890905101286888838646440667 + var1.mul_assign(&var12); + // 357 : 1345194418404962471943721421137207912880369630951403025128450748508963241180442836686101344 + square!(var1, 5); + // 362 : 1345194418404962471943721421137207912880369630951403025128450748508963241180442836686101367 + var1.mul_assign(&var7); + // 363 : 43046221388958799102199085476390653212171828190444896804110423952286823717774170773955243744 + square!(var1, 5); + // 368 : 43046221388958799102199085476390653212171828190444896804110423952286823717774170773955243749 + var1.mul_assign(&var15); + // 369 : 5509916337786726285081482940978003611157994008376946790926134265892713435875093859066271199872 + square!(var1, 7); + // 376 : 5509916337786726285081482940978003611157994008376946790926134265892713435875093859066271199899 + var1.mul_assign(&var12); + // 377 : 176317322809175241122607454111296115557055808268062297309636296508566829948003003490120678396768 + square!(var1, 5); + // 382 : 176317322809175241122607454111296115557055808268062297309636296508566829948003003490120678396791 + var1.mul_assign(&var7); + // 383 : 5642154329893607715923438531561475697825785864577993513908361488274138558336096111683861708697312 + square!(var1, 5); + // 388 : 5642154329893607715923438531561475697825785864577993513908361488274138558336096111683861708697333 + var1.mul_assign(&var4); + // 389 : 90274469278297723454775016504983611165212573833247896222533783812386216933377537786941787339157328 + square!(var1, 4); + // 393 : 90274469278297723454775016504983611165212573833247896222533783812386216933377537786941787339157331 + var1.mul_assign(&var2); + // 394 : 5777566033811054301105601056318951114573604725327865358242162163992717883736162418364274389706069184 + square!(var1, 6); + // 400 : 5777566033811054301105601056318951114573604725327865358242162163992717883736162418364274389706069189 + var1.mul_assign(&var15); + // 401 : 369764226163907475270758467604412871332710702420983382927498378495533944559114394775313560941188428096 + square!(var1, 6); + // 407 : 369764226163907475270758467604412871332710702420983382927498378495533944559114394775313560941188428105 + var1.mul_assign(&var14); + // 408 : 5916227618622519604332135481670605941323371238735734126839974055928543112945830316405016975059014849680 + square!(var1, 4); + // 412 : 5916227618622519604332135481670605941323371238735734126839974055928543112945830316405016975059014849683 + var1.mul_assign(&var2); + // 413 : 94659641897960313669314167706729695061173939819771746029439584894856689807133285062480271600944237594928 + square!(var1, 4); + // 417 : 94659641897960313669314167706729695061173939819771746029439584894856689807133285062480271600944237594931 + var1.mul_assign(&var2); + // 418 : 24232868325877840299344426932922801935660528593861566983536533733083312590626120975994949529841724824302336 + square!(var1, 8); + // 426 : 24232868325877840299344426932922801935660528593861566983536533733083312590626120975994949529841724824302345 + var1.mul_assign(&var14); + // 427 : 775451786428090889579021661853529661941136915003570143473169079458666002900035871231838384954935194377675040 + square!(var1, 5); + // 432 : 775451786428090889579021661853529661941136915003570143473169079458666002900035871231838384954935194377675055 + var1.mul_assign(&var10); + // 433 : 49628914331397816933057386358625898364232762560228489182282821085354624185602295758837656637115852440171203520 + square!(var1, 6); + // 439 : 49628914331397816933057386358625898364232762560228489182282821085354624185602295758837656637115852440171203527 + var1.mul_assign(&var3); + // 440 : 1588125258604730141857836363476028747655448401927311653833050274731347973939273464282805012387707278085478512864 + square!(var1, 5); + // 445 : 1588125258604730141857836363476028747655448401927311653833050274731347973939273464282805012387707278085478512879 + var1.mul_assign(&var10); + // 446 : 6504961059244974661049697744797813750396716654294268534100173925299601301255264109702369330740049011038119988752384 + square!(var1, 12); + // 458 : 6504961059244974661049697744797813750396716654294268534100173925299601301255264109702369330740049011038119988752401 + var1.mul_assign(&var9); + // 459 : 104079376947919594576795163916765020006347466468708296545602782804793620820084225755237909291840784176609919820038416 + square!(var1, 4); + // 463 : 104079376947919594576795163916765020006347466468708296545602782804793620820084225755237909291840784176609919820038429 + var1.mul_assign(&var5); + // 464 : 3330540062333427026457445245336480640203118926998665489459289049753395866242695224167613097338905093651517434241229728 + square!(var1, 5); + // 469 : 3330540062333427026457445245336480640203118926998665489459289049753395866242695224167613097338905093651517434241229741 + var1.mul_assign(&var5); + // 470 : 213154563989339329693276495701534760972999611327914591325394499184217335439532494346727238229689925993697115791438703424 + square!(var1, 6); + // 476 : 213154563989339329693276495701534760972999611327914591325394499184217335439532494346727238229689925993697115791438703427 + var1.mul_assign(&var2); + // 477 : 109135136762541736802957565799185797618175800999892270758601983582319275745040637105524345973601242108772923285216616154624 + square!(var1, 9); + // 486 : 109135136762541736802957565799185797618175800999892270758601983582319275745040637105524345973601242108772923285216616154649 + var1.mul_assign(&var6); + // 487 : 3492324376401335577694642105573945523781625631996552664275263474634216823841300387376779071155239747480733545126931716948768 + square!(var1, 5); + // 492 : 3492324376401335577694642105573945523781625631996552664275263474634216823841300387376779071155239747480733545126931716948793 + var1.mul_assign(&var6); + // 493 : 223508760089685476972457094756732513522024040447779370513616862376589876725843224792113860553935343838766946888123629884722752 + square!(var1, 6); + // 499 : 223508760089685476972457094756732513522024040447779370513616862376589876725843224792113860553935343838766946888123629884722755 + var1.mul_assign(&var2); + // 500 : 14304560645739870526237254064430880865409538588657879712871479192101752110453966386695287075451862005681084600839912312622256320 + square!(var1, 6); + // 506 : 14304560645739870526237254064430880865409538588657879712871479192101752110453966386695287075451862005681084600839912312622256323 + var1.mul_assign(&var2); + // 507 : 7323935050618813709433474080988611003089683757392834412990197346356097080552430789987986982631353346908715315630035104062595237376 + square!(var1, 9); + // 516 : 7323935050618813709433474080988611003089683757392834412990197346356097080552430789987986982631353346908715315630035104062595237399 + var1.mul_assign(&var7); + // 517 : 937463686479208154807484682366542208395479520946282804862745260333580426310711141118462333776813228404315560400644493320012190387072 + square!(var1, 7); + // 524 : 937463686479208154807484682366542208395479520946282804862745260333580426310711141118462333776813228404315560400644493320012190387087 + var1.mul_assign(&var10); + // 525 : 59997675934669321907679019671458701337310689340562099511215696661349147283885513031581589361716046617876195865641247572480780184773568 + square!(var1, 6); + // 531 : 59997675934669321907679019671458701337310689340562099511215696661349147283885513031581589361716046617876195865641247572480780184773593 + var1.mul_assign(&var6); + // 532 : 1919925629909418301045728629486678442793942058897987184358902293163172713084336417010610859574913491772038267700519922319384965912754976 + square!(var1, 5); + // 537 : 1919925629909418301045728629486678442793942058897987184358902293163172713084336417010610859574913491772038267700519922319384965912754985 + var1.mul_assign(&var14); + // 538 : 245750480628405542533853264574294840677624583538942359597939493524886107274795061377358190025588926946820898265666550056881275636832638080 + square!(var1, 7); + // 545 : 245750480628405542533853264574294840677624583538942359597939493524886107274795061377358190025588926946820898265666550056881275636832638103 + var1.mul_assign(&var7); + // 546 : 983001922513622170135413058297179362710498334155769438391757974099544429099180245509432760102355707787283593062666200227525102547330552412 + square!(var1, 2); + // 548 : 983001922513622170135413058297179362710498334155769438391757974099544429099180245509432760102355707787283593062666200227525102547330552413 + var1.mul_assign(var0); + // 549 : 251648492163487275554665742924077916853887573543876976228290041369483373849390142850414786586203061193544599824042547258246426252116621417728 + square!(var1, 8); + // 557 : 251648492163487275554665742924077916853887573543876976228290041369483373849390142850414786586203061193544599824042547258246426252116621417739 + var1.mul_assign(&var13); + // 558 : 4026375874615796408874651886785246669662201176702031619652640661911733981590242285606636585379248979096713597184680756131942820033865942683824 + square!(var1, 4); + // 562 : 4026375874615796408874651886785246669662201176702031619652640661911733981590242285606636585379248979096713597184680756131942820033865942683829 + var1.mul_assign(&var15); + // 563 : 515376111950821940335955441508511573716761750617860047315538004724701949643551012557649482928543869324379340439639136784888680964334840663530112 + square!(var1, 7); + // 570 : 515376111950821940335955441508511573716761750617860047315538004724701949643551012557649482928543869324379340439639136784888680964334840663530119 + var1.mul_assign(&var3); + // 571 : 131936284659410416726004593026178962871491008158172172112777729209523699108749059214758267629707230547041111152547619016931502326869719209863710464 + square!(var1, 8); + // 579 : 131936284659410416726004593026178962871491008158172172112777729209523699108749059214758267629707230547041111152547619016931502326869719209863710473 + var1.mul_assign(&var14); + // 580 : 16887844436404533340928587907350907247550849044246038030435549338819033485919879579489058256602525510021262227526095234167232297839324058862554940544 + square!(var1, 7); + // 587 : 16887844436404533340928587907350907247550849044246038030435549338819033485919879579489058256602525510021262227526095234167232297839324058862554940557 + var1.mul_assign(&var5); + // 588 : 17293152702878242141110874017127329021492069421307942943166002522950690289581956689396795654760986122261772520986721519787245872987467836275256259130368 + square!(var1, 10); + // 598 : 17293152702878242141110874017127329021492069421307942943166002522950690289581956689396795654760986122261772520986721519787245872987467836275256259130377 + var1.mul_assign(&var14); + // 599 : 1106761772984207497031095937096149057375492442963708348362624161468844178533245228121394921904703111824753441343150177266383735871197941521616400584344128 + square!(var1, 6); + // 605 : 1106761772984207497031095937096149057375492442963708348362624161468844178533245228121394921904703111824753441343150177266383735871197941521616400584344139 + var1.mul_assign(&var13); + // 606 : 70832753470989279809990139974153539672031516349677334295207946334006027426127694599769275001900999156784220245961611345048559095756668257383449637398024896 + square!(var1, 6); + // 612 : 70832753470989279809990139974153539672031516349677334295207946334006027426127694599769275001900999156784220245961611345048559095756668257383449637398024909 + var1.mul_assign(&var5); + // 613 : 4533296222143313907839368958345826539010017046379349394893308565376385755272172454385233600121663946034190095741543126083107782128426768472540776793473594176 + square!(var1, 6); + // 619 : 4533296222143313907839368958345826539010017046379349394893308565376385755272172454385233600121663946034190095741543126083107782128426768472540776793473594207 + var1.mul_assign(&var11); + // 620 : 145065479108586045050859806667066449248320545484139180636585874092044344168709518540327475203893246273094083063729380034659449028109656591121304857391155014624 + square!(var1, 5); + // 625 : 145065479108586045050859806667066449248320545484139180636585874092044344168709518540327475203893246273094083063729380034659449028109656591121304857391155014649 + var1.mul_assign(&var6); + // 626 : 18568381325899013766510055253384505503785029821969815121482991883781676053594818373161916826098335522956042632157360644436409475598036043663527021746067841875072 + square!(var1, 7); + // 633 : 18568381325899013766510055253384505503785029821969815121482991883781676053594818373161916826098335522956042632157360644436409475598036043663527021746067841875087 + var1.mul_assign(&var10); + // 634 : 594188202428768440528321768108304176121120954303034083887455740281013633715034187941181338435146736734593364229035540621965103219137153397232864695874170940002784 + square!(var1, 5); + // 639 : 594188202428768440528321768108304176121120954303034083887455740281013633715034187941181338435146736734593364229035540621965103219137153397232864695874170940002797 + var1.mul_assign(&var5); + // 640 : 76056089910882360387625186317862934543503482150788362737594334755969745115524376056471211319698782302027950621316549199611533212049555634845806681071893880320358016 + square!(var1, 7); + // 647 : 76056089910882360387625186317862934543503482150788362737594334755969745115524376056471211319698782302027950621316549199611533212049555634845806681071893880320358047 + var1.mul_assign(&var11); + // 648 : 2433794877148235532404005962171613905392111428825227607603018712191031843696780033807078762230361033664894419882129574387569062785585780315065813794300604170251457504 + square!(var1, 5); + // 653 : 2433794877148235532404005962171613905392111428825227607603018712191031843696780033807078762230361033664894419882129574387569062785585780315065813794300604170251457511 + var1.mul_assign(&var3); + // 654 : 623051488549948296295425526315933159780380525779258267546372790320904151986375688654612163130972424618212971489825171043217680073109959760656848331340954667584373122816 + square!(var1, 8); + // 662 : 623051488549948296295425526315933159780380525779258267546372790320904151986375688654612163130972424618212971489825171043217680073109959760656848331340954667584373122843 + var1.mul_assign(&var12); + // 663 : 39875295267196690962907233684219722225944353649872529122967858580537865727128044073895178440382235175565630175348810946765931524679037424682038293205821098725399879861952 + square!(var1, 6); + // 669 : 39875295267196690962907233684219722225944353649872529122967858580537865727128044073895178440382235175565630175348810946765931524679037424682038293205821098725399879861981 + var1.mul_assign(&var8); + // 670 : 2552018897100588221626062955790062222460438633591841863869942949154423406536194820729291420184463051236200331222323900593019617579458395179650450765172550318425592311166784 + square!(var1, 6); + // 676 : 2552018897100588221626062955790062222460438633591841863869942949154423406536194820729291420184463051236200331222323900593019617579458395179650450765172550318425592311166787 + var1.mul_assign(&var2); + // 677 : 326658418828875292368136058341127964474936145099755758575352697491766196036632937053349301783611270558233642396457459275906511050170674582995257697942086440758475815829348736 + square!(var1, 7); + // 684 : 326658418828875292368136058341127964474936145099755758575352697491766196036632937053349301783611270558233642396457459275906511050170674582995257697942086440758475815829348747 + var1.mul_assign(&var13); + // 685 : 41812277610096037423121415467664379452791826572768737097645145278946073092689015942828710628302242631453906226746554787316033414421846346623392985336587064417084904426156639616 + square!(var1, 7); + // 692 : 41812277610096037423121415467664379452791826572768737097645145278946073092689015942828710628302242631453906226746554787316033414421846346623392985336587064417084904426156639627 + var1.mul_assign(&var13); + // 693 : 2675985767046146395079770589930520284978676900657199174249289297852548677932097020341037480211343528413049998511779506388226138522998166183897151061541572122693433883274024936128 + square!(var1, 6); + // 699 : 2675985767046146395079770589930520284978676900657199174249289297852548677932097020341037480211343528413049998511779506388226138522998166183897151061541572122693433883274024936131 + var1.mul_assign(&var2); + // 700 : 85631544545476684642552658877776649119317660821030373575977257531281557693827104650913199366762992909217599952376944204423236432735941317884708833969330307926189884264768797956192 + square!(var1, 5); + // 705 : 85631544545476684642552658877776649119317660821030373575977257531281557693827104650913199366762992909217599952376944204423236432735941317884708833969330307926189884264768797956199 + var1.mul_assign(&var3); + // 706 : 87686701614568125073973922690843288698181284680735102541800711712032315078478955162535116151565304739038822351233990865329394107121603909513941845984594235316418441487123249107147776 + square!(var1, 10); + // 716 : 87686701614568125073973922690843288698181284680735102541800711712032315078478955162535116151565304739038822351233990865329394107121603909513941845984594235316418441487123249107147803 + var1.mul_assign(&var12); + // 717 : 1402987225833090001183582763053492619170900554891761640668811387392517041255663282600561858425044875824621157619743853845270305713945662552223069535753507765062695063793971985714364848 + square!(var1, 4); + // 721 : 1402987225833090001183582763053492619170900554891761640668811387392517041255663282600561858425044875824621157619743853845270305713945662552223069535753507765062695063793971985714364849 + var1.mul_assign(var0); + // 722 : 718329459626542080605994374683388221015501084104581960022431430344968725122899600691487671513622976422206032701308853168778396525540179226738211602305795975712099872662513656685754802688 + square!(var1, 9); + // 731 : 718329459626542080605994374683388221015501084104581960022431430344968725122899600691487671513622976422206032701308853168778396525540179226738211602305795975712099872662513656685754802705 + var1.mul_assign(&var9); + // 732 : 45973085416098693158783639979736846144992069382693245441435611542077998407865574444255210976871870491021186092883766602801817377634571470511245542547570942445574391850400874027888307373120 + square!(var1, 6); + // 738 : 45973085416098693158783639979736846144992069382693245441435611542077998407865574444255210976871870491021186092883766602801817377634571470511245542547570942445574391850400874027888307373135 + var1.mul_assign(&var10); + // 739 : 5884554933260632724324305917406316306558984880984735416503758277385983796206793528864667005039599422850711819889122125158632624337225148225439429446089080633033522156851311875569703343761280 + square!(var1, 7); + // 746 : 5884554933260632724324305917406316306558984880984735416503758277385983796206793528864667005039599422850711819889122125158632624337225148225439429446089080633033522156851311875569703343761311 + var1.mul_assign(&var11); + // 747 : 188305757864340247178377789357002121809887516191511533328120264876351481478617392923669344161267181531222778236451908005076243978791204743214061742274850580257072709019241980018230507000361952 + square!(var1, 5); + // 752 : 188305757864340247178377789357002121809887516191511533328120264876351481478617392923669344161267181531222778236451908005076243978791204743214061742274850580257072709019241980018230507000361973 + var1.mul_assign(&var4); + // 753 : 3012892125829443954854044629712033948958200259064184533249924238021623703657878286778709506580274904499564451783230528081219903660659275891424987876397609284113163344307871680291688112005791568 + square!(var1, 4); + // 757 : 3012892125829443954854044629712033948958200259064184533249924238021623703657878286778709506580274904499564451783230528081219903660659275891424987876397609284113163344307871680291688112005791583 + var1.mul_assign(&var10); + // 758 : 385650192106168826221317712603140345466649633160215620255990302466767834068208420707674816842275187775944249828253507594396147668564387314102398448178893988366484908071407575077336078336741322624 + square!(var1, 7); + // 765 : 385650192106168826221317712603140345466649633160215620255990302466767834068208420707674816842275187775944249828253507594396147668564387314102398448178893988366484908071407575077336078336741322653 + var1.mul_assign(&var8); + // 766 : 12340806147397402439082166803300491054932788261126899848191689678936570690182669462645594138952806008830215994504112243020676725394060394051276750341724607627727517058285042402474754506775722324896 + square!(var1, 5); + // 771 : 12340806147397402439082166803300491054932788261126899848191689678936570690182669462645594138952806008830215994504112243020676725394060394051276750341724607627727517058285042402474754506775722324917 + var1.mul_assign(&var4); + // 772 : 394905796716716878050629337705615713757849224356060795142134069725970262085845422804659012446489792282566911824131591776661655212609932609640856010935187444087280545865121356879192144216823114397344 + square!(var1, 5); + // 777 : 394905796716716878050629337705615713757849224356060795142134069725970262085845422804659012446489792282566911824131591776661655212609932609640856010935187444087280545865121356879192144216823114397365 + var1.mul_assign(&var4); + // 778 : 12636985494934940097620138806579702840251175179393945444548290231231048386747053529749088398287673353042141178372210936853172966803517843508507392349925998210792977467683883420134148614938339660715680 + square!(var1, 5); + // 783 : 12636985494934940097620138806579702840251175179393945444548290231231048386747053529749088398287673353042141178372210936853172966803517843508507392349925998210792977467683883420134148614938339660715697 + var1.mul_assign(&var9); + // 784 : 202191767918959041561922220905275245444018802870303127112772643699696774187952856475985414372602773648674258853955374989650767468856285496136118277598815971372687639482942134722146377839013434571451152 + square!(var1, 4); + // 788 : 202191767918959041561922220905275245444018802870303127112772643699696774187952856475985414372602773648674258853955374989650767468856285496136118277598815971372687639482942134722146377839013434571451165 + var1.mul_assign(&var5); + // 789 : 12940273146813378659963022137937615708417203383699400135217449196780593548028982814463066519846577513515152566653143999337649118006802271752711569766324222167852008926908296622217368181696859812572874560 + square!(var1, 6); + // 795 : 12940273146813378659963022137937615708417203383699400135217449196780593548028982814463066519846577513515152566653143999337649118006802271752711569766324222167852008926908296622217368181696859812572874589 + var1.mul_assign(&var8); + // 796 : 25880546293626757319926044275875231416834406767398800270434898393561187096057965628926133039693155027030305133306287998675298236013604543505423139532648444335704017853816593244434736363393719625145749178 + var1 = var1.square(); + // 797 : 25880546293626757319926044275875231416834406767398800270434898393561187096057965628926133039693155027030305133306287998675298236013604543505423139532648444335704017853816593244434736363393719625145749179 + var1.mul_assign(var0); + // 798 : 1656354962792112468475266833656014810677402033113523217307833497187915974147709800251272514540361921729939528531602431915219087104870690784347080930089500437485057142644261967643823127257198056009327947456 + square!(var1, 6); + // 804 : 1656354962792112468475266833656014810677402033113523217307833497187915974147709800251272514540361921729939528531602431915219087104870690784347080930089500437485057142644261967643823127257198056009327947463 + var1.mul_assign(&var3); + // 805 : 1696107481899123167718673237663759166133659681908247774523221501120425957527254835457303054889330607851458077216360890281184345195387587363171410872411648447984698514067724254867274882311370809353551818202112 + square!(var1, 10); + // 815 : 1696107481899123167718673237663759166133659681908247774523221501120425957527254835457303054889330607851458077216360890281184345195387587363171410872411648447984698514067724254867274882311370809353551818202135 + var1.mul_assign(&var7); + // 816 : 108550878841543882733995087210480586632554219642127857569486176071707261281744309469267395512917158902493316941847096977995798092504805591242970295834345500671020704900334352311505592467927731798627316364936640 + square!(var1, 6); + // 822 : 108550878841543882733995087210480586632554219642127857569486176071707261281744309469267395512917158902493316941847096977995798092504805591242970295834345500671020704900334352311505592467927731798627316364936661 + var1.mul_assign(&var4); + // 823 : 6947256245858808494975685581470757544483470057096182884447115268589264722031635806033113312826698169759572284278214206591731077920307557839550098933398112042945325113621398547936357917947374835112148247355946304 + square!(var1, 6); + // 829 : 6947256245858808494975685581470757544483470057096182884447115268589264722031635806033113312826698169759572284278214206591731077920307557839550098933398112042945325113621398547936357917947374835112148247355946329 + var1.mul_assign(&var6); + // 830 : 444624399734963743678443877214128482846942083654155704604615377189712942210024691586119252020908682864612626193805709221870788986899683701731206331737479170748500807271769507067926906748631989447177487830780565056 + square!(var1, 6); + // 836 : 444624399734963743678443877214128482846942083654155704604615377189712942210024691586119252020908682864612626193805709221870788986899683701731206331737479170748500807271769507067926906748631989447177487830780565069 + var1.mul_assign(&var5); + // 837 : 28455961583037679595420408141704222902204293353865965094695384140141628301441580261511632129338155703335208076403565390199730495161579756910797205231198666927904051665393248452347322031912447324619359221169956164416 + square!(var1, 6); + // 843 : 28455961583037679595420408141704222902204293353865965094695384140141628301441580261511632129338155703335208076403565390199730495161579756910797205231198666927904051665393248452347322031912447324619359221169956164437 + var1.mul_assign(&var4); + // 844 : 238705906983162543355580399100765177871214152862586865721082456961065184302499251714358569373223087638243353151383559860752580829556389241459968722180074986980751351032731127113348364375477010926880553717580063640645533696 + square!(var1, 23); + // 867 : 238705906983162543355580399100765177871214152862586865721082456961065184302499251714358569373223087638243353151383559860752580829556389241459968722180074986980751351032731127113348364375477010926880553717580063640645533703 + var1.mul_assign(&var3); + // 868 : 15277178046922402774757145542448971383757705783205559406149277245508171795359952109718948439886277608847574601688547831088165173091608911453437998219524799166768086466094792135254295320030528699320355437925124073001314156992 + square!(var1, 6); + // 874 : 15277178046922402774757145542448971383757705783205559406149277245508171795359952109718948439886277608847574601688547831088165173091608911453437998219524799166768086466094792135254295320030528699320355437925124073001314156999 + var1.mul_assign(&var3); + // 875 : 488869697501516888792228657358367084280246585062577900996776871856261497451518467511006350076360883483122387254033530594821285538931485166510015943024793573336578766915033348328137450240976918378251374013603970336042053023968 + square!(var1, 5); + // 880 : 488869697501516888792228657358367084280246585062577900996776871856261497451518467511006350076360883483122387254033530594821285538931485166510015943024793573336578766915033348328137450240976918378251374013603970336042053023971 + var1.mul_assign(&var2); + // 881 : 31287660640097080882702634070935493393935781444004985663793719798800735836897181920704406404887096542919832784258145958068562274491615050656641020353586788693541041082562134293000796815422522776208087936870654101506691393534144 + square!(var1, 6); + // 887 : 31287660640097080882702634070935493393935781444004985663793719798800735836897181920704406404887096542919832784258145958068562274491615050656641020353586788693541041082562134293000796815422522776208087936870654101506691393534151 + var1.mul_assign(&var3); + // 888 : 1001205140483106588246484290269935788605945006208159541241399033561623546780709821462541004956387089373434649096260670658193992783731681621012512651314777238193313314641988297376025498093520728838658813979860931248214124593092832 + square!(var1, 5); + // 893 : 1001205140483106588246484290269935788605945006208159541241399033561623546780709821462541004956387089373434649096260670658193992783731681621012512651314777238193313314641988297376025498093520728838658813979860931248214124593092835 + var1 * var2 +} + +/// Tests for addition chains +#[cfg(test)] +mod tests { + use super::*; + use rand_core::SeedableRng; + + const SEED: [u8; 16] = [ + 0x59, 0x62, 0xbe, 0x5d, 0x76, 0x3d, 0x31, 0x8d, 0x17, 0xdb, 0x37, 0x32, 0x54, 0x06, 0xbc, + 0xe5, + ]; + + #[test] + fn test_fp_chain() { + let mut rng = rand_xorshift::XorShiftRng::from_seed(SEED); + let p_m3_over4 = [ + 0xee7f_bfff_ffff_eaaa, + 0x07aa_ffff_ac54_ffff, + 0xd9cc_34a8_3dac_3d89, + 0xd91d_d2e1_3ce1_44af, + 0x92c6_e9ed_90d2_eb35, + 0x0680_447a_8e5f_f9a6, + ]; + + for _ in 0..32 { + let input = Fp::random(&mut rng); + assert_eq!(chain_pm3div4(&input), input.pow_vartime(&p_m3_over4)); + } + } + + #[test] + fn test_fp2_chain() { + let mut rng = rand_xorshift::XorShiftRng::from_seed(SEED); + let p_sq_m9_over16 = [ + 0xb26a_a000_01c7_18e3, + 0xd7ce_d6b1_d763_82ea, + 0x3162_c338_3621_13cf, + 0x966b_f91e_d3e7_1b74, + 0xb292_e85a_8709_1a04, + 0x11d6_8619_c861_85c7, + 0xef53_1493_3097_8ef0, + 0x050a_62cf_d16d_dca6, + 0x466e_59e4_9349_e8bd, + 0x9e2d_c90e_50e7_046b, + 0x74bd_278e_aa22_f25e, + 0x002a_437a_4b8c_35fc, + ]; + + for _ in 0..32 { + let input = Fp2::random(&mut rng); + assert_eq!( + chain_p2m9div16(&input), + input.pow_vartime_extended(&p_sq_m9_over16[..]), + ); + } + } +} diff --git a/constantine/bls12_381/src/hash_to_curve/expand_msg.rs b/constantine/bls12_381/src/hash_to_curve/expand_msg.rs new file mode 100644 index 000000000..7187d46b7 --- /dev/null +++ b/constantine/bls12_381/src/hash_to_curve/expand_msg.rs @@ -0,0 +1,1282 @@ +//! This module implements message expansion consistent with the +//! hash-to-curve RFC drafts 7 through 10 + +use core::{ + fmt::{self, Debug, Formatter}, + marker::PhantomData, +}; + +use digest::{BlockInput, Digest, ExtendableOutputDirty, Update, XofReader}; + +use crate::generic_array::{ + typenum::{Unsigned, U32}, + ArrayLength, GenericArray, +}; + +#[cfg(feature = "alloc")] +use alloc::vec::Vec; + +const OVERSIZE_DST_SALT: &[u8] = b"H2C-OVERSIZE-DST-"; + +/// The domain separation tag for a message expansion. +/// +/// Implements [section 5.4.3 of `draft-irtf-cfrg-hash-to-curve-12`][dst]. +/// +/// [dst]: https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-12#section-5.4.3 +#[derive(Debug)] +enum ExpandMsgDst<'x, L: ArrayLength> { + /// DST produced by hashing a very long (> 255 chars) input DST. + Hashed(GenericArray), + /// A raw input DST (<= 255 chars). + Raw(&'x [u8]), +} + +impl<'x, L: ArrayLength> ExpandMsgDst<'x, L> { + /// Produces a DST for use with `expand_message_xof`. + pub fn process_xof(dst: &'x [u8]) -> Self + where + H: Default + Update + ExtendableOutputDirty, + { + if dst.len() > 255 { + let mut data = GenericArray::::default(); + H::default() + .chain(OVERSIZE_DST_SALT) + .chain(&dst) + .finalize_xof_dirty() + .read(&mut data); + Self::Hashed(data) + } else { + Self::Raw(dst) + } + } + + /// Produces a DST for use with `expand_message_xmd`. + pub fn process_xmd(dst: &'x [u8]) -> Self + where + H: Digest, + { + if dst.len() > 255 { + Self::Hashed(H::new().chain(OVERSIZE_DST_SALT).chain(&dst).finalize()) + } else { + Self::Raw(dst) + } + } + + /// Returns the raw bytes of the DST. + pub fn data(&'x self) -> &'x [u8] { + match self { + Self::Hashed(arr) => &arr[..], + Self::Raw(buf) => buf, + } + } + + /// Returns the length of the DST. + pub fn len(&'x self) -> usize { + match self { + Self::Hashed(_) => L::to_usize(), + Self::Raw(buf) => buf.len(), + } + } +} + +/// A trait for message expansion methods supported by hash-to-curve. +pub trait ExpandMessage: for<'x> InitExpandMessage<'x> { + // This intermediate is likely only necessary until GATs allow + // associated types with lifetimes. +} + +/// Trait for constructing a new message expander. +pub trait InitExpandMessage<'x> { + /// The state object used during message expansion. + type Expander: ExpandMessageState<'x>; + + /// Initializes a message expander. + fn init_expand(message: &[u8], dst: &'x [u8], len_in_bytes: usize) -> Self::Expander; +} + +// Automatically derive trait +impl InitExpandMessage<'x>> ExpandMessage for X {} + +/// Trait for types implementing the `expand_message` interface for `hash_to_field`. +pub trait ExpandMessageState<'x> { + /// Reads bytes from the generated output. + fn read_into(&mut self, output: &mut [u8]) -> usize; + + /// Retrieves the number of bytes remaining in the generator. + fn remain(&self) -> usize; + + #[cfg(feature = "alloc")] + /// Constructs a `Vec` containing the remaining bytes of the output. + fn into_vec(mut self) -> Vec + where + Self: Sized, + { + let mut result = alloc::vec![0u8; self.remain()]; + self.read_into(&mut result[..]); + result + } +} + +/// A generator for the output of `expand_message_xof` for a given +/// extendable hash function, message, DST, and output length. +/// +/// Implements [section 5.4.2 of `draft-irtf-cfrg-hash-to-curve-12`][expand_message_xof] +/// with `k = 128`. +/// +/// [expand_message_xof]: https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-12#section-5.4.2 +pub struct ExpandMsgXof { + hash: ::Reader, + remain: usize, +} + +impl Debug for ExpandMsgXof { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_struct("ExpandMsgXof") + .field("remain", &self.remain) + .finish() + } +} + +impl<'x, H> ExpandMessageState<'x> for ExpandMsgXof +where + H: ExtendableOutputDirty, +{ + fn read_into(&mut self, output: &mut [u8]) -> usize { + let len = self.remain.min(output.len()); + self.hash.read(&mut output[..len]); + self.remain -= len; + len + } + + fn remain(&self) -> usize { + self.remain + } +} + +impl<'x, H> InitExpandMessage<'x> for ExpandMsgXof +where + H: Default + Update + ExtendableOutputDirty, +{ + type Expander = Self; + + fn init_expand(message: &[u8], dst: &[u8], len_in_bytes: usize) -> Self { + // Use U32 here for k = 128. + let dst = ExpandMsgDst::::process_xof::(dst); + let hash = H::default() + .chain(message) + .chain((len_in_bytes as u16).to_be_bytes()) + .chain(dst.data()) + .chain([dst.len() as u8]) + .finalize_xof_dirty(); + Self { + hash, + remain: len_in_bytes, + } + } +} + +/// Constructor for `expand_message_xmd` for a given digest hash function, message, DST, +/// and output length. +/// +/// Implements [section 5.4.1 of `draft-irtf-cfrg-hash-to-curve-12`][expand_message_xmd]. +/// +/// [expand_message_xmd]: https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-12#section-5.4.1 +#[derive(Debug)] +pub struct ExpandMsgXmd(PhantomData); + +/// A generator for the output of `expand_message_xmd` for a given +/// digest hash function, message, DST, and output length. +/// +/// Implements [section 5.4.1 of `draft-irtf-cfrg-hash-to-curve-12`][expand_message_xmd]. +/// +/// [expand_message_xmd]: https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-12#section-5.4.1 +pub struct ExpandMsgXmdState<'x, H: Digest> { + dst: ExpandMsgDst<'x, H::OutputSize>, + b_0: GenericArray, + b_i: GenericArray, + i: usize, + b_offs: usize, + remain: usize, +} + +impl Debug for ExpandMsgXmdState<'_, H> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_struct("ExpandMsgXmdState") + .field("remain", &self.remain) + .finish() + } +} + +impl<'x, H> InitExpandMessage<'x> for ExpandMsgXmd +where + H: Digest + BlockInput, +{ + type Expander = ExpandMsgXmdState<'x, H>; + + fn init_expand(message: &[u8], dst: &'x [u8], len_in_bytes: usize) -> Self::Expander { + let hash_size = ::OutputSize::to_usize(); + let ell = (len_in_bytes + hash_size - 1) / hash_size; + if ell > 255 { + panic!("Invalid ExpandMsgXmd usage: ell > 255"); + } + let dst = ExpandMsgDst::process_xmd::(dst); + let b_0 = H::new() + .chain(GenericArray::::BlockSize>::default()) + .chain(message) + .chain((len_in_bytes as u16).to_be_bytes()) + .chain([0u8]) + .chain(dst.data()) + .chain([dst.len() as u8]) + .finalize(); + // init with b_1 + let b_i = H::new() + .chain(&b_0) + .chain([1u8]) + .chain(dst.data()) + .chain([dst.len() as u8]) + .finalize(); + ExpandMsgXmdState { + dst, + b_0, + b_i, + i: 2, + b_offs: 0, + remain: len_in_bytes, + } + } +} + +impl<'x, H> ExpandMessageState<'x> for ExpandMsgXmdState<'x, H> +where + H: Digest + BlockInput, +{ + fn read_into(&mut self, output: &mut [u8]) -> usize { + let read_len = self.remain.min(output.len()); + let mut offs = 0; + let hash_size = H::OutputSize::to_usize(); + while offs < read_len { + let b_offs = self.b_offs; + let mut copy_len = hash_size - b_offs; + if copy_len > 0 { + copy_len = copy_len.min(read_len - offs); + output[offs..(offs + copy_len)] + .copy_from_slice(&self.b_i[b_offs..(b_offs + copy_len)]); + offs += copy_len; + self.b_offs = b_offs + copy_len; + } else { + let mut b_prev_xor = self.b_0.clone(); + for j in 0..hash_size { + b_prev_xor[j] ^= self.b_i[j]; + } + self.b_i = H::new() + .chain(b_prev_xor) + .chain([self.i as u8]) + .chain(self.dst.data()) + .chain([self.dst.len() as u8]) + .finalize(); + self.b_offs = 0; + self.i += 1; + } + } + self.remain -= read_len; + read_len + } + + fn remain(&self) -> usize { + self.remain + } +} + +#[cfg(feature = "alloc")] +#[cfg(test)] +mod tests { + use super::*; + use sha2::{Sha256, Sha512}; + use sha3::{Shake128, Shake256}; + + /// From + #[test] + fn expand_message_xmd_works_for_draft12_testvectors_sha256() { + let dst = b"QUUX-V01-CS02-with-expander-SHA256-128"; + + let msg = b""; + let len_in_bytes = 0x20; + let uniform_bytes = hex::decode( + "68a985b87eb6b46952128911f2a4412bbc302a9d759667f8\ + 7f7a21d803f07235", + ) + .unwrap(); + assert_eq!( + ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b"abc"; + let len_in_bytes = 0x20; + let uniform_bytes = hex::decode( + "d8ccab23b5985ccea865c6c97b6e5b8350e794e603b4b979\ + 02f53a8a0d605615", + ) + .unwrap(); + assert_eq!( + ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b"abcdef0123456789"; + let len_in_bytes = 0x20; + let uniform_bytes = hex::decode( + "eff31487c770a893cfb36f912fbfcbff40d5661771ca4b2c\ + b4eafe524333f5c1", + ) + .unwrap(); + assert_eq!( + ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b"q128_qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ + qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ + qqqqqqqqqqqqqqqqqqqqqqqqq"; + let len_in_bytes = 0x20; + let uniform_bytes = hex::decode( + "b23a1d2b4d97b2ef7785562a7e8bac7eed54ed6e97e29aa5\ + 1bfe3f12ddad1ff9", + ) + .unwrap(); + assert_eq!( + ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b"a512_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + let len_in_bytes = 0x20; + let uniform_bytes = hex::decode( + "4623227bcc01293b8c130bf771da8c298dede7383243dc09\ + 93d2d94823958c4c", + ) + .unwrap(); + assert_eq!( + ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b""; + let len_in_bytes = 0x80; + let uniform_bytes = hex::decode( + "af84c27ccfd45d41914fdff5df25293e221afc53d8ad2ac0\ + 6d5e3e29485dadbee0d121587713a3e0dd4d5e69e93eb7cd4f5df4\ + cd103e188cf60cb02edc3edf18eda8576c412b18ffb658e3dd6ec8\ + 49469b979d444cf7b26911a08e63cf31f9dcc541708d3491184472\ + c2c29bb749d4286b004ceb5ee6b9a7fa5b646c993f0ced", + ) + .unwrap(); + assert_eq!( + ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b"abc"; + let len_in_bytes = 0x80; + let uniform_bytes = hex::decode( + "abba86a6129e366fc877aab32fc4ffc70120d8996c88aee2\ + fe4b32d6c7b6437a647e6c3163d40b76a73cf6a5674ef1d890f95b\ + 664ee0afa5359a5c4e07985635bbecbac65d747d3d2da7ec2b8221\ + b17b0ca9dc8a1ac1c07ea6a1e60583e2cb00058e77b7b72a298425\ + cd1b941ad4ec65e8afc50303a22c0f99b0509b4c895f40", + ) + .unwrap(); + assert_eq!( + ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b"abcdef0123456789"; + let len_in_bytes = 0x80; + let uniform_bytes = hex::decode( + "ef904a29bffc4cf9ee82832451c946ac3c8f8058ae97d8d6\ + 29831a74c6572bd9ebd0df635cd1f208e2038e760c4994984ce73f\ + 0d55ea9f22af83ba4734569d4bc95e18350f740c07eef653cbb9f8\ + 7910d833751825f0ebefa1abe5420bb52be14cf489b37fe1a72f7d\ + e2d10be453b2c9d9eb20c7e3f6edc5a60629178d9478df", + ) + .unwrap(); + assert_eq!( + ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b"q128_qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ + qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ + qqqqqqqqqqqqqqqqqqqqqqqqq"; + let len_in_bytes = 0x80; + let uniform_bytes = hex::decode( + "80be107d0884f0d881bb460322f0443d38bd222db8bd0b0a\ + 5312a6fedb49c1bbd88fd75d8b9a09486c60123dfa1d73c1cc3169\ + 761b17476d3c6b7cbbd727acd0e2c942f4dd96ae3da5de368d26b3\ + 2286e32de7e5a8cb2949f866a0b80c58116b29fa7fabb3ea7d520e\ + e603e0c25bcaf0b9a5e92ec6a1fe4e0391d1cdbce8c68a", + ) + .unwrap(); + assert_eq!( + ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b"a512_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + let len_in_bytes = 0x80; + let uniform_bytes = hex::decode( + "546aff5444b5b79aa6148bd81728704c32decb73a3ba76e9\ + e75885cad9def1d06d6792f8a7d12794e90efed817d96920d72889\ + 6a4510864370c207f99bd4a608ea121700ef01ed879745ee3e4cee\ + f777eda6d9e5e38b90c86ea6fb0b36504ba4a45d22e86f6db5dd43\ + d98a294bebb9125d5b794e9d2a81181066eb954966a487", + ) + .unwrap(); + assert_eq!( + ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + } + + /// From + #[test] + fn expand_message_xmd_works_for_draft12_testvectors_sha256_long_dst() { + let dst = b"QUUX-V01-CS02-with-expander-SHA256-128-long-DST-111111\ + 111111111111111111111111111111111111111111111111111111\ + 111111111111111111111111111111111111111111111111111111\ + 111111111111111111111111111111111111111111111111111111\ + 1111111111111111111111111111111111111111"; + + let msg = b""; + let len_in_bytes = 0x20; + let uniform_bytes = hex::decode( + "e8dc0c8b686b7ef2074086fbdd2f30e3f8bfbd3bdf177f73\ + f04b97ce618a3ed3", + ) + .unwrap(); + assert_eq!( + ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b"abc"; + let len_in_bytes = 0x20; + let uniform_bytes = hex::decode( + "52dbf4f36cf560fca57dedec2ad924ee9c266341d8f3d6af\ + e5171733b16bbb12", + ) + .unwrap(); + assert_eq!( + ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b"abcdef0123456789"; + let len_in_bytes = 0x20; + let uniform_bytes = hex::decode( + "35387dcf22618f3728e6c686490f8b431f76550b0b2c61cb\ + c1ce7001536f4521", + ) + .unwrap(); + assert_eq!( + ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b"q128_qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ + qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ + qqqqqqqqqqqqqqqqqqqqqqqqq"; + let len_in_bytes = 0x20; + let uniform_bytes = hex::decode( + "01b637612bb18e840028be900a833a74414140dde0c4754c\ + 198532c3a0ba42bc", + ) + .unwrap(); + assert_eq!( + ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b"a512_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + let len_in_bytes = 0x20; + let uniform_bytes = hex::decode( + "20cce7033cabc5460743180be6fa8aac5a103f56d481cf36\ + 9a8accc0c374431b", + ) + .unwrap(); + assert_eq!( + ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b""; + let len_in_bytes = 0x80; + let uniform_bytes = hex::decode( + "14604d85432c68b757e485c8894db3117992fc57e0e136f7\ + 1ad987f789a0abc287c47876978e2388a02af86b1e8d1342e5ce4f\ + 7aaa07a87321e691f6fba7e0072eecc1218aebb89fb14a0662322d\ + 5edbd873f0eb35260145cd4e64f748c5dfe60567e126604bcab1a3\ + ee2dc0778102ae8a5cfd1429ebc0fa6bf1a53c36f55dfc", + ) + .unwrap(); + assert_eq!( + ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b"abc"; + let len_in_bytes = 0x80; + let uniform_bytes = hex::decode( + "1a30a5e36fbdb87077552b9d18b9f0aee16e80181d5b951d\ + 0471d55b66684914aef87dbb3626eaabf5ded8cd0686567e503853\ + e5c84c259ba0efc37f71c839da2129fe81afdaec7fbdc0ccd4c794\ + 727a17c0d20ff0ea55e1389d6982d1241cb8d165762dbc39fb0cee\ + 4474d2cbbd468a835ae5b2f20e4f959f56ab24cd6fe267", + ) + .unwrap(); + assert_eq!( + ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b"abcdef0123456789"; + let len_in_bytes = 0x80; + let uniform_bytes = hex::decode( + "d2ecef3635d2397f34a9f86438d772db19ffe9924e28a1ca\ + f6f1c8f15603d4028f40891044e5c7e39ebb9b31339979ff33a424\ + 9206f67d4a1e7c765410bcd249ad78d407e303675918f20f26ce6d\ + 7027ed3774512ef5b00d816e51bfcc96c3539601fa48ef1c07e494\ + bdc37054ba96ecb9dbd666417e3de289d4f424f502a982", + ) + .unwrap(); + assert_eq!( + ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b"q128_qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ + qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ + qqqqqqqqqqqqqqqqqqqqqqqqq"; + let len_in_bytes = 0x80; + let uniform_bytes = hex::decode( + "ed6e8c036df90111410431431a232d41a32c86e296c05d42\ + 6e5f44e75b9a50d335b2412bc6c91e0a6dc131de09c43110d9180d\ + 0a70f0d6289cb4e43b05f7ee5e9b3f42a1fad0f31bac6a625b3b5c\ + 50e3a83316783b649e5ecc9d3b1d9471cb5024b7ccf40d41d1751a\ + 04ca0356548bc6e703fca02ab521b505e8e45600508d32", + ) + .unwrap(); + assert_eq!( + ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b"a512_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + let len_in_bytes = 0x80; + let uniform_bytes = hex::decode( + "78b53f2413f3c688f07732c10e5ced29a17c6a16f717179f\ + fbe38d92d6c9ec296502eb9889af83a1928cd162e845b0d3c5424e\ + 83280fed3d10cffb2f8431f14e7a23f4c68819d40617589e4c4116\ + 9d0b56e0e3535be1fd71fbb08bb70c5b5ffed953d6c14bf7618b35\ + fc1f4c4b30538236b4b08c9fbf90462447a8ada60be495", + ) + .unwrap(); + assert_eq!( + ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + } + + /// From + #[test] + fn expand_message_xmd_works_for_draft12_testvectors_sha512() { + let dst = b"QUUX-V01-CS02-with-expander-SHA512-256"; + + let msg = b""; + let len_in_bytes = 0x20; + let uniform_bytes = hex::decode( + "6b9a7312411d92f921c6f68ca0b6380730a1a4d982c50721\ + 1a90964c394179ba", + ) + .unwrap(); + assert_eq!( + ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b"abc"; + let len_in_bytes = 0x20; + let uniform_bytes = hex::decode( + "0da749f12fbe5483eb066a5f595055679b976e93abe9be6f\ + 0f6318bce7aca8dc", + ) + .unwrap(); + assert_eq!( + ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b"abcdef0123456789"; + let len_in_bytes = 0x20; + let uniform_bytes = hex::decode( + "087e45a86e2939ee8b91100af1583c4938e0f5fc6c9db4b1\ + 07b83346bc967f58", + ) + .unwrap(); + assert_eq!( + ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b"q128_qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ + qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ + qqqqqqqqqqqqqqqqqqqqqqqqq"; + let len_in_bytes = 0x20; + let uniform_bytes = hex::decode( + "7336234ee9983902440f6bc35b348352013becd88938d2af\ + ec44311caf8356b3", + ) + .unwrap(); + assert_eq!( + ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b"a512_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + let len_in_bytes = 0x20; + let uniform_bytes = hex::decode( + "57b5f7e766d5be68a6bfe1768e3c2b7f1228b3e4b3134956\ + dd73a59b954c66f4", + ) + .unwrap(); + assert_eq!( + ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b""; + let len_in_bytes = 0x80; + let uniform_bytes = hex::decode( + "41b037d1734a5f8df225dd8c7de38f851efdb45c372887be\ + 655212d07251b921b052b62eaed99b46f72f2ef4cc96bfaf254ebb\ + bec091e1a3b9e4fb5e5b619d2e0c5414800a1d882b62bb5cd1778f\ + 098b8eb6cb399d5d9d18f5d5842cf5d13d7eb00a7cff859b605da6\ + 78b318bd0e65ebff70bec88c753b159a805d2c89c55961", + ) + .unwrap(); + assert_eq!( + ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b"abc"; + let len_in_bytes = 0x80; + let uniform_bytes = hex::decode( + "7f1dddd13c08b543f2e2037b14cefb255b44c83cc397c178\ + 6d975653e36a6b11bdd7732d8b38adb4a0edc26a0cef4bb4521713\ + 5456e58fbca1703cd6032cb1347ee720b87972d63fbf232587043e\ + d2901bce7f22610c0419751c065922b488431851041310ad659e4b\ + 23520e1772ab29dcdeb2002222a363f0c2b1c972b3efe1", + ) + .unwrap(); + assert_eq!( + ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b"abcdef0123456789"; + let len_in_bytes = 0x80; + let uniform_bytes = hex::decode( + "3f721f208e6199fe903545abc26c837ce59ac6fa45733f1b\ + aaf0222f8b7acb0424814fcb5eecf6c1d38f06e9d0a6ccfbf85ae6\ + 12ab8735dfdf9ce84c372a77c8f9e1c1e952c3a61b7567dd069301\ + 6af51d2745822663d0c2367e3f4f0bed827feecc2aaf98c949b5ed\ + 0d35c3f1023d64ad1407924288d366ea159f46287e61ac", + ) + .unwrap(); + assert_eq!( + ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b"q128_qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ + qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ + qqqqqqqqqqqqqqqqqqqqqqqqq"; + let len_in_bytes = 0x80; + let uniform_bytes = hex::decode( + "b799b045a58c8d2b4334cf54b78260b45eec544f9f2fb5bd\ + 12fb603eaee70db7317bf807c406e26373922b7b8920fa29142703\ + dd52bdf280084fb7ef69da78afdf80b3586395b433dc66cde048a2\ + 58e476a561e9deba7060af40adf30c64249ca7ddea79806ee5beb9\ + a1422949471d267b21bc88e688e4014087a0b592b695ed", + ) + .unwrap(); + assert_eq!( + ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b"a512_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + let len_in_bytes = 0x80; + let uniform_bytes = hex::decode( + "05b0bfef265dcee87654372777b7c44177e2ae4c13a27f10\ + 3340d9cd11c86cb2426ffcad5bd964080c2aee97f03be1ca18e30a\ + 1f14e27bc11ebbd650f305269cc9fb1db08bf90bfc79b42a952b46\ + daf810359e7bc36452684784a64952c343c52e5124cd1f71d474d5\ + 197fefc571a92929c9084ffe1112cf5eea5192ebff330b", + ) + .unwrap(); + assert_eq!( + ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + } + + /// From + #[test] + fn expand_message_xof_works_for_draft12_testvectors_shake128() { + let dst = b"QUUX-V01-CS02-with-expander-SHAKE128"; + + let msg = b""; + let len_in_bytes = 0x20; + let uniform_bytes = hex::decode( + "86518c9cd86581486e9485aa74ab35ba150d1c75c88e26b7\ + 043e44e2acd735a2", + ) + .unwrap(); + assert_eq!( + ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b"abc"; + let len_in_bytes = 0x20; + let uniform_bytes = hex::decode( + "8696af52a4d862417c0763556073f47bc9b9ba43c99b5053\ + 05cb1ec04a9ab468", + ) + .unwrap(); + assert_eq!( + ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b"abcdef0123456789"; + let len_in_bytes = 0x20; + let uniform_bytes = hex::decode( + "912c58deac4821c3509dbefa094df54b34b8f5d01a191d1d\ + 3108a2c89077acca", + ) + .unwrap(); + assert_eq!( + ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b"q128_qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ + qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ + qqqqqqqqqqqqqqqqqqqqqqqqq"; + let len_in_bytes = 0x20; + let uniform_bytes = hex::decode( + "1adbcc448aef2a0cebc71dac9f756b22e51839d348e031e6\ + 3b33ebb50faeaf3f", + ) + .unwrap(); + assert_eq!( + ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b"a512_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + let len_in_bytes = 0x20; + let uniform_bytes = hex::decode( + "df3447cc5f3e9a77da10f819218ddf31342c310778e0e4ef\ + 72bbaecee786a4fe", + ) + .unwrap(); + assert_eq!( + ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b""; + let len_in_bytes = 0x80; + let uniform_bytes = hex::decode( + "7314ff1a155a2fb99a0171dc71b89ab6e3b2b7d59e38e644\ + 19b8b6294d03ffee42491f11370261f436220ef787f8f76f5b26bd\ + cd850071920ce023f3ac46847744f4612b8714db8f5db83205b2e6\ + 25d95afd7d7b4d3094d3bdde815f52850bb41ead9822e08f22cf41\ + d615a303b0d9dde73263c049a7b9898208003a739a2e57", + ) + .unwrap(); + assert_eq!( + ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b"abc"; + let len_in_bytes = 0x80; + let uniform_bytes = hex::decode( + "c952f0c8e529ca8824acc6a4cab0e782fc3648c563ddb00d\ + a7399f2ae35654f4860ec671db2356ba7baa55a34a9d7f79197b60\ + ddae6e64768a37d699a78323496db3878c8d64d909d0f8a7de4927\ + dcab0d3dbbc26cb20a49eceb0530b431cdf47bc8c0fa3e0d88f53b\ + 318b6739fbed7d7634974f1b5c386d6230c76260d5337a", + ) + .unwrap(); + assert_eq!( + ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b"abcdef0123456789"; + let len_in_bytes = 0x80; + let uniform_bytes = hex::decode( + "19b65ee7afec6ac06a144f2d6134f08eeec185f1a890fe34\ + e68f0e377b7d0312883c048d9b8a1d6ecc3b541cb4987c26f45e0c\ + 82691ea299b5e6889bbfe589153016d8131717ba26f07c3c14ffbe\ + f1f3eff9752e5b6183f43871a78219a75e7000fbac6a7072e2b83c\ + 790a3a5aecd9d14be79f9fd4fb180960a3772e08680495", + ) + .unwrap(); + assert_eq!( + ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b"q128_qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ + qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ + qqqqqqqqqqqqqqqqqqqqqqqqq"; + let len_in_bytes = 0x80; + let uniform_bytes = hex::decode( + "ca1b56861482b16eae0f4a26212112362fcc2d76dcc80c93\ + c4182ed66c5113fe41733ed68be2942a3487394317f3379856f482\ + 2a611735e50528a60e7ade8ec8c71670fec6661e2c59a09ed36386\ + 513221688b35dc47e3c3111ee8c67ff49579089d661caa29db1ef1\ + 0eb6eace575bf3dc9806e7c4016bd50f3c0e2a6481ee6d", + ) + .unwrap(); + assert_eq!( + ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b"a512_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + let len_in_bytes = 0x80; + let uniform_bytes = hex::decode( + "9d763a5ce58f65c91531b4100c7266d479a5d9777ba76169\ + 3d052acd37d149e7ac91c796a10b919cd74a591a1e38719fb91b72\ + 03e2af31eac3bff7ead2c195af7d88b8bc0a8adf3d1e90ab9bed6d\ + dc2b7f655dd86c730bdeaea884e73741097142c92f0e3fc1811b69\ + 9ba593c7fbd81da288a29d423df831652e3a01a9374999", + ) + .unwrap(); + assert_eq!( + ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + } + + /// From + #[test] + fn expand_message_xof_works_for_draft12_testvectors_shake128_long_dst() { + let dst = b"QUUX-V01-CS02-with-expander-SHAKE128-long-DST-11111111\ + 111111111111111111111111111111111111111111111111111111\ + 111111111111111111111111111111111111111111111111111111\ + 111111111111111111111111111111111111111111111111111111\ + 1111111111111111111111111111111111111111"; + + let msg = b""; + let len_in_bytes = 0x20; + let uniform_bytes = hex::decode( + "827c6216330a122352312bccc0c8d6e7a146c5257a776dbd\ + 9ad9d75cd880fc53", + ) + .unwrap(); + assert_eq!( + ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b"abc"; + let len_in_bytes = 0x20; + let uniform_bytes = hex::decode( + "690c8d82c7213b4282c6cb41c00e31ea1d3e2005f93ad19b\ + bf6da40f15790c5c", + ) + .unwrap(); + assert_eq!( + ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b"abcdef0123456789"; + let len_in_bytes = 0x20; + let uniform_bytes = hex::decode( + "979e3a15064afbbcf99f62cc09fa9c85028afcf3f825eb07\ + 11894dcfc2f57057", + ) + .unwrap(); + assert_eq!( + ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b"q128_qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ + qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ + qqqqqqqqqqqqqqqqqqqqqqqqq"; + let len_in_bytes = 0x20; + let uniform_bytes = hex::decode( + "c5a9220962d9edc212c063f4f65b609755a1ed96e62f9db5\ + d1fd6adb5a8dc52b", + ) + .unwrap(); + assert_eq!( + ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b"a512_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + let len_in_bytes = 0x20; + let uniform_bytes = hex::decode( + "f7b96a5901af5d78ce1d071d9c383cac66a1dfadb508300e\ + c6aeaea0d62d5d62", + ) + .unwrap(); + assert_eq!( + ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b""; + let len_in_bytes = 0x80; + let uniform_bytes = hex::decode( + "3890dbab00a2830be398524b71c2713bbef5f4884ac2e6f0\ + 70b092effdb19208c7df943dc5dcbaee3094a78c267ef276632ee2\ + c8ea0c05363c94b6348500fae4208345dd3475fe0c834c2beac7fa\ + 7bc181692fb728c0a53d809fc8111495222ce0f38468b11becb15b\ + 32060218e285c57a60162c2c8bb5b6bded13973cd41819", + ) + .unwrap(); + assert_eq!( + ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b"abc"; + let len_in_bytes = 0x80; + let uniform_bytes = hex::decode( + "41b7ffa7a301b5c1441495ebb9774e2a53dbbf4e54b9a1af\ + 6a20fd41eafd69ef7b9418599c5545b1ee422f363642b01d4a5344\ + 9313f68da3e49dddb9cd25b97465170537d45dcbdf92391b5bdff3\ + 44db4bd06311a05bca7dcd360b6caec849c299133e5c9194f4e15e\ + 3e23cfaab4003fab776f6ac0bfae9144c6e2e1c62e7d57", + ) + .unwrap(); + assert_eq!( + ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b"abcdef0123456789"; + let len_in_bytes = 0x80; + let uniform_bytes = hex::decode( + "55317e4a21318472cd2290c3082957e1242241d9e0d04f47\ + 026f03401643131401071f01aa03038b2783e795bdfa8a3541c194\ + ad5de7cb9c225133e24af6c86e748deb52e560569bd54ef4dac034\ + 65111a3a44b0ea490fb36777ff8ea9f1a8a3e8e0de3cf0880b4b2f\ + 8dd37d3a85a8b82375aee4fa0e909f9763319b55778e71", + ) + .unwrap(); + assert_eq!( + ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b"q128_qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ + qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ + qqqqqqqqqqqqqqqqqqqqqqqqq"; + let len_in_bytes = 0x80; + let uniform_bytes = hex::decode( + "19fdd2639f082e31c77717ac9bb032a22ff0958382b2dbb3\ + 9020cdc78f0da43305414806abf9a561cb2d0067eb2f7bc544482f\ + 75623438ed4b4e39dd9e6e2909dd858bd8f1d57cd0fce2d3150d90\ + aa67b4498bdf2df98c0100dd1a173436ba5d0df6be1defb0b2ce55\ + ccd2f4fc05eb7cb2c019c35d5398b85adc676da4238bc7", + ) + .unwrap(); + assert_eq!( + ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b"a512_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + let len_in_bytes = 0x80; + let uniform_bytes = hex::decode( + "945373f0b3431a103333ba6a0a34f1efab2702efde41754c\ + 4cb1d5216d5b0a92a67458d968562bde7fa6310a83f53dda138368\ + 0a276a283438d58ceebfa7ab7ba72499d4a3eddc860595f63c93b1\ + c5e823ea41fc490d938398a26db28f61857698553e93f0574eb8c5\ + 017bfed6249491f9976aaa8d23d9485339cc85ca329308", + ) + .unwrap(); + assert_eq!( + ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + } + + /// From + #[test] + fn expand_message_xof_works_for_draft12_testvectors_shake256() { + let dst = b"QUUX-V01-CS02-with-expander-SHAKE256"; + + let msg = b""; + let len_in_bytes = 0x20; + let uniform_bytes = hex::decode( + "2ffc05c48ed32b95d72e807f6eab9f7530dd1c2f013914c8\ + fed38c5ccc15ad76", + ) + .unwrap(); + assert_eq!( + ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b"abc"; + let len_in_bytes = 0x20; + let uniform_bytes = hex::decode( + "b39e493867e2767216792abce1f2676c197c0692aed06156\ + 0ead251821808e07", + ) + .unwrap(); + assert_eq!( + ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b"abcdef0123456789"; + let len_in_bytes = 0x20; + let uniform_bytes = hex::decode( + "245389cf44a13f0e70af8665fe5337ec2dcd138890bb7901\ + c4ad9cfceb054b65", + ) + .unwrap(); + assert_eq!( + ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b"q128_qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ + qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ + qqqqqqqqqqqqqqqqqqqqqqqqq"; + let len_in_bytes = 0x20; + let uniform_bytes = hex::decode( + "719b3911821e6428a5ed9b8e600f2866bcf23c8f0515e52d\ + 6c6c019a03f16f0e", + ) + .unwrap(); + assert_eq!( + ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b"a512_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + let len_in_bytes = 0x20; + let uniform_bytes = hex::decode( + "9181ead5220b1963f1b5951f35547a5ea86a820562287d6c\ + a4723633d17ccbbc", + ) + .unwrap(); + assert_eq!( + ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b""; + let len_in_bytes = 0x80; + let uniform_bytes = hex::decode( + "7a1361d2d7d82d79e035b8880c5a3c86c5afa719478c007d\ + 96e6c88737a3f631dd74a2c88df79a4cb5e5d9f7504957c70d669e\ + c6bfedc31e01e2bacc4ff3fdf9b6a00b17cc18d9d72ace7d6b81c2\ + e481b4f73f34f9a7505dccbe8f5485f3d20c5409b0310093d5d649\ + 2dea4e18aa6979c23c8ea5de01582e9689612afbb353df", + ) + .unwrap(); + assert_eq!( + ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b"abc"; + let len_in_bytes = 0x80; + let uniform_bytes = hex::decode( + "a54303e6b172909783353ab05ef08dd435a558c3197db0c1\ + 32134649708e0b9b4e34fb99b92a9e9e28fc1f1d8860d85897a8e0\ + 21e6382f3eea10577f968ff6df6c45fe624ce65ca25932f679a42a\ + 404bc3681efe03fcd45ef73bb3a8f79ba784f80f55ea8a3c367408\ + f30381299617f50c8cf8fbb21d0f1e1d70b0131a7b6fbe", + ) + .unwrap(); + assert_eq!( + ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b"abcdef0123456789"; + let len_in_bytes = 0x80; + let uniform_bytes = hex::decode( + "e42e4d9538a189316e3154b821c1bafb390f78b2f010ea40\ + 4e6ac063deb8c0852fcd412e098e231e43427bd2be1330bb47b403\ + 9ad57b30ae1fc94e34993b162ff4d695e42d59d9777ea18d3848d9\ + d336c25d2acb93adcad009bcfb9cde12286df267ada283063de0bb\ + 1505565b2eb6c90e31c48798ecdc71a71756a9110ff373", + ) + .unwrap(); + assert_eq!( + ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b"q128_qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ + qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ + qqqqqqqqqqqqqqqqqqqqqqqqq"; + let len_in_bytes = 0x80; + let uniform_bytes = hex::decode( + "4ac054dda0a38a65d0ecf7afd3c2812300027c8789655e47\ + aecf1ecc1a2426b17444c7482c99e5907afd9c25b991990490bb9c\ + 686f43e79b4471a23a703d4b02f23c669737a886a7ec28bddb92c3\ + a98de63ebf878aa363a501a60055c048bea11840c4717beae7eee2\ + 8c3cfa42857b3d130188571943a7bd747de831bd6444e0", + ) + .unwrap(); + assert_eq!( + ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + + let msg = b"a512_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + let len_in_bytes = 0x80; + let uniform_bytes = hex::decode( + "09afc76d51c2cccbc129c2315df66c2be7295a231203b8ab\ + 2dd7f95c2772c68e500bc72e20c602abc9964663b7a03a389be128\ + c56971ce81001a0b875e7fd17822db9d69792ddf6a23a151bf4700\ + 79c518279aef3e75611f8f828994a9988f4a8a256ddb8bae161e65\ + 8d5a2a09bcfe839c6396dc06ee5c8ff3c22d3b1f9deb7e", + ) + .unwrap(); + assert_eq!( + ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), + uniform_bytes + ); + } +} diff --git a/constantine/bls12_381/src/hash_to_curve/map_g1.rs b/constantine/bls12_381/src/hash_to_curve/map_g1.rs new file mode 100644 index 000000000..0f2c94e6e --- /dev/null +++ b/constantine/bls12_381/src/hash_to_curve/map_g1.rs @@ -0,0 +1,961 @@ +//! Implementation of hash-to-curve for the G1 group. + +use subtle::{Choice, ConditionallyNegatable, ConditionallySelectable, ConstantTimeEq}; + +use super::chain::chain_pm3div4; +use super::{HashToField, MapToCurve, Sgn0}; +use crate::fp::Fp; +use crate::g1::G1Projective; +use crate::generic_array::{typenum::U64, GenericArray}; + +/// Coefficients of the 11-isogeny x map's numerator +const ISO11_XNUM: [Fp; 12] = [ + Fp::from_raw_unchecked([ + 0x4d18_b6f3_af00_131c, + 0x19fa_2197_93fe_e28c, + 0x3f28_85f1_467f_19ae, + 0x23dc_ea34_f2ff_b304, + 0xd15b_58d2_ffc0_0054, + 0x0913_be20_0a20_bef4, + ]), + Fp::from_raw_unchecked([ + 0x8989_8538_5cdb_bd8b, + 0x3c79_e43c_c7d9_66aa, + 0x1597_e193_f4cd_233a, + 0x8637_ef1e_4d66_23ad, + 0x11b2_2dee_d20d_827b, + 0x0709_7bc5_9987_84ad, + ]), + Fp::from_raw_unchecked([ + 0xa542_583a_480b_664b, + 0xfc71_69c0_26e5_68c6, + 0x5ba2_ef31_4ed8_b5a6, + 0x5b54_91c0_5102_f0e7, + 0xdf6e_9970_7d2a_0079, + 0x0784_151e_d760_5524, + ]), + Fp::from_raw_unchecked([ + 0x494e_2128_70f7_2741, + 0xab9b_e52f_bda4_3021, + 0x26f5_5779_94e3_4c3d, + 0x049d_fee8_2aef_bd60, + 0x65da_dd78_2850_5289, + 0x0e93_d431_ea01_1aeb, + ]), + Fp::from_raw_unchecked([ + 0x90ee_774b_d6a7_4d45, + 0x7ada_1c8a_41bf_b185, + 0x0f1a_8953_b325_f464, + 0x104c_2421_1be4_805c, + 0x1691_39d3_19ea_7a8f, + 0x09f2_0ead_8e53_2bf6, + ]), + Fp::from_raw_unchecked([ + 0x6ddd_93e2_f436_26b7, + 0xa548_2c9a_a1cc_d7bd, + 0x1432_4563_1883_f4bd, + 0x2e0a_94cc_f77e_c0db, + 0xb028_2d48_0e56_489f, + 0x18f4_bfcb_b436_8929, + ]), + Fp::from_raw_unchecked([ + 0x23c5_f0c9_5340_2dfd, + 0x7a43_ff69_58ce_4fe9, + 0x2c39_0d3d_2da5_df63, + 0xd0df_5c98_e1f9_d70f, + 0xffd8_9869_a572_b297, + 0x1277_ffc7_2f25_e8fe, + ]), + Fp::from_raw_unchecked([ + 0x79f4_f049_0f06_a8a6, + 0x85f8_94a8_8030_fd81, + 0x12da_3054_b18b_6410, + 0xe2a5_7f65_0588_0d65, + 0xbba0_74f2_60e4_00f1, + 0x08b7_6279_f621_d028, + ]), + Fp::from_raw_unchecked([ + 0xe672_45ba_78d5_b00b, + 0x8456_ba9a_1f18_6475, + 0x7888_bff6_e6b3_3bb4, + 0xe215_85b9_a30f_86cb, + 0x05a6_9cdc_ef55_feee, + 0x09e6_99dd_9adf_a5ac, + ]), + Fp::from_raw_unchecked([ + 0x0de5_c357_bff5_7107, + 0x0a0d_b4ae_6b1a_10b2, + 0xe256_bb67_b3b3_cd8d, + 0x8ad4_5657_4e9d_b24f, + 0x0443_915f_50fd_4179, + 0x098c_4bf7_de8b_6375, + ]), + Fp::from_raw_unchecked([ + 0xe6b0_617e_7dd9_29c7, + 0xfe6e_37d4_4253_7375, + 0x1daf_deda_137a_489e, + 0xe4ef_d1ad_3f76_7ceb, + 0x4a51_d866_7f0f_e1cf, + 0x054f_df4b_bf1d_821c, + ]), + Fp::from_raw_unchecked([ + 0x72db_2a50_658d_767b, + 0x8abf_91fa_a257_b3d5, + 0xe969_d683_3764_ab47, + 0x4641_7014_2a10_09eb, + 0xb14f_01aa_db30_be2f, + 0x18ae_6a85_6f40_715d, + ]), +]; + +/// Coefficients of the 11-isogeny x map's denominator +const ISO11_XDEN: [Fp; 11] = [ + Fp::from_raw_unchecked([ + 0xb962_a077_fdb0_f945, + 0xa6a9_740f_efda_13a0, + 0xc14d_568c_3ed6_c544, + 0xb43f_c37b_908b_133e, + 0x9c0b_3ac9_2959_9016, + 0x0165_aa6c_93ad_115f, + ]), + Fp::from_raw_unchecked([ + 0x2327_9a3b_a506_c1d9, + 0x92cf_ca0a_9465_176a, + 0x3b29_4ab1_3755_f0ff, + 0x116d_da1c_5070_ae93, + 0xed45_3092_4cec_2045, + 0x0833_83d6_ed81_f1ce, + ]), + Fp::from_raw_unchecked([ + 0x9885_c2a6_449f_ecfc, + 0x4a2b_54cc_d377_33f0, + 0x17da_9ffd_8738_c142, + 0xa0fb_a727_32b3_fafd, + 0xff36_4f36_e54b_6812, + 0x0f29_c13c_6605_23e2, + ]), + Fp::from_raw_unchecked([ + 0xe349_cc11_8278_f041, + 0xd487_228f_2f32_04fb, + 0xc9d3_2584_9ade_5150, + 0x43a9_2bd6_9c15_c2df, + 0x1c2c_7844_bc41_7be4, + 0x1202_5184_f407_440c, + ]), + Fp::from_raw_unchecked([ + 0x587f_65ae_6acb_057b, + 0x1444_ef32_5140_201f, + 0xfbf9_95e7_1270_da49, + 0xccda_0660_7243_6a42, + 0x7408_904f_0f18_6bb2, + 0x13b9_3c63_edf6_c015, + ]), + Fp::from_raw_unchecked([ + 0xfb91_8622_cd14_1920, + 0x4a4c_6442_3eca_ddb4, + 0x0beb_2329_27f7_fb26, + 0x30f9_4df6_f83a_3dc2, + 0xaeed_d424_d780_f388, + 0x06cc_402d_d594_bbeb, + ]), + Fp::from_raw_unchecked([ + 0xd41f_7611_51b2_3f8f, + 0x32a9_2465_4357_19b3, + 0x64f4_36e8_88c6_2cb9, + 0xdf70_a9a1_f757_c6e4, + 0x6933_a38d_5b59_4c81, + 0x0c6f_7f72_37b4_6606, + ]), + Fp::from_raw_unchecked([ + 0x693c_0874_7876_c8f7, + 0x22c9_850b_f9cf_80f0, + 0x8e90_71da_b950_c124, + 0x89bc_62d6_1c7b_af23, + 0xbc6b_e2d8_dad5_7c23, + 0x1791_6987_aa14_a122, + ]), + Fp::from_raw_unchecked([ + 0x1be3_ff43_9c13_16fd, + 0x9965_243a_7571_dfa7, + 0xc7f7_f629_62f5_cd81, + 0x32c6_aa9a_f394_361c, + 0xbbc2_ee18_e1c2_27f4, + 0x0c10_2cba_c531_bb34, + ]), + Fp::from_raw_unchecked([ + 0x9976_14c9_7bac_bf07, + 0x61f8_6372_b991_92c0, + 0x5b8c_95fc_1435_3fc3, + 0xca2b_066c_2a87_492f, + 0x1617_8f5b_bf69_8711, + 0x12a6_dcd7_f0f4_e0e8, + ]), + Fp::from_raw_unchecked([ + 0x7609_0000_0002_fffd, + 0xebf4_000b_c40c_0002, + 0x5f48_9857_53c7_58ba, + 0x77ce_5853_7052_5745, + 0x5c07_1a97_a256_ec6d, + 0x15f6_5ec3_fa80_e493, + ]), +]; + +/// Coefficients of the 11-isogeny y map's numerator +const ISO11_YNUM: [Fp; 16] = [ + Fp::from_raw_unchecked([ + 0x2b56_7ff3_e283_7267, + 0x1d4d_9e57_b958_a767, + 0xce02_8fea_04bd_7373, + 0xcc31_a30a_0b6c_d3df, + 0x7d7b_18a6_8269_2693, + 0x0d30_0744_d42a_0310, + ]), + Fp::from_raw_unchecked([ + 0x99c2_555f_a542_493f, + 0xfe7f_53cc_4874_f878, + 0x5df0_608b_8f97_608a, + 0x14e0_3832_052b_49c8, + 0x7063_26a6_957d_d5a4, + 0x0a8d_add9_c241_4555, + ]), + Fp::from_raw_unchecked([ + 0x13d9_4292_2a5c_f63a, + 0x357e_33e3_6e26_1e7d, + 0xcf05_a27c_8456_088d, + 0x0000_bd1d_e7ba_50f0, + 0x83d0_c753_2f8c_1fde, + 0x13f7_0bf3_8bbf_2905, + ]), + Fp::from_raw_unchecked([ + 0x5c57_fd95_bfaf_bdbb, + 0x28a3_59a6_5e54_1707, + 0x3983_ceb4_f636_0b6d, + 0xafe1_9ff6_f97e_6d53, + 0xb346_8f45_5019_2bf7, + 0x0bb6_cde4_9d8b_a257, + ]), + Fp::from_raw_unchecked([ + 0x590b_62c7_ff8a_513f, + 0x314b_4ce3_72ca_cefd, + 0x6bef_32ce_94b8_a800, + 0x6ddf_84a0_9571_3d5f, + 0x64ea_ce4c_b098_2191, + 0x0386_213c_651b_888d, + ]), + Fp::from_raw_unchecked([ + 0xa531_0a31_111b_bcdd, + 0xa14a_c0f5_da14_8982, + 0xf9ad_9cc9_5423_d2e9, + 0xaa6e_c095_283e_e4a7, + 0xcf5b_1f02_2e1c_9107, + 0x01fd_df5a_ed88_1793, + ]), + Fp::from_raw_unchecked([ + 0x65a5_72b0_d7a7_d950, + 0xe25c_2d81_8347_3a19, + 0xc2fc_ebe7_cb87_7dbd, + 0x05b2_d36c_769a_89b0, + 0xba12_961b_e86e_9efb, + 0x07eb_1b29_c1df_de1f, + ]), + Fp::from_raw_unchecked([ + 0x93e0_9572_f7c4_cd24, + 0x364e_9290_7679_5091, + 0x8569_467e_68af_51b5, + 0xa47d_a894_39f5_340f, + 0xf4fa_9180_82e4_4d64, + 0x0ad5_2ba3_e669_5a79, + ]), + Fp::from_raw_unchecked([ + 0x9114_2984_4e0d_5f54, + 0xd03f_51a3_516b_b233, + 0x3d58_7e56_4053_6e66, + 0xfa86_d2a3_a9a7_3482, + 0xa90e_d5ad_f1ed_5537, + 0x149c_9c32_6a5e_7393, + ]), + Fp::from_raw_unchecked([ + 0x462b_beb0_3c12_921a, + 0xdc9a_f5fa_0a27_4a17, + 0x9a55_8ebd_e836_ebed, + 0x649e_f8f1_1a4f_ae46, + 0x8100_e165_2b3c_dc62, + 0x1862_bd62_c291_dacb, + ]), + Fp::from_raw_unchecked([ + 0x05c9_b8ca_89f1_2c26, + 0x0194_160f_a9b9_ac4f, + 0x6a64_3d5a_6879_fa2c, + 0x1466_5bdd_8846_e19d, + 0xbb1d_0d53_af3f_f6bf, + 0x12c7_e1c3_b289_62e5, + ]), + Fp::from_raw_unchecked([ + 0xb55e_bf90_0b8a_3e17, + 0xfedc_77ec_1a92_01c4, + 0x1f07_db10_ea1a_4df4, + 0x0dfb_d15d_c41a_594d, + 0x3895_47f2_334a_5391, + 0x0241_9f98_1658_71a4, + ]), + Fp::from_raw_unchecked([ + 0xb416_af00_0745_fc20, + 0x8e56_3e9d_1ea6_d0f5, + 0x7c76_3e17_763a_0652, + 0x0145_8ef0_159e_bbef, + 0x8346_fe42_1f96_bb13, + 0x0d2d_7b82_9ce3_24d2, + ]), + Fp::from_raw_unchecked([ + 0x9309_6bb5_38d6_4615, + 0x6f2a_2619_951d_823a, + 0x8f66_b3ea_5951_4fa4, + 0xf563_e637_04f7_092f, + 0x724b_136c_4cf2_d9fa, + 0x0469_59cf_cfd0_bf49, + ]), + Fp::from_raw_unchecked([ + 0xea74_8d4b_6e40_5346, + 0x91e9_079c_2c02_d58f, + 0x4106_4965_946d_9b59, + 0xa067_31f1_d2bb_e1ee, + 0x07f8_97e2_67a3_3f1b, + 0x1017_2909_1921_0e5f, + ]), + Fp::from_raw_unchecked([ + 0x872a_a6c1_7d98_5097, + 0xeecc_5316_1264_562a, + 0x07af_e37a_fff5_5002, + 0x5475_9078_e5be_6838, + 0xc4b9_2d15_db8a_cca8, + 0x106d_87d1_b51d_13b9, + ]), +]; + +/// Coefficients of the 11-isogeny y map's denominator +const ISO11_YDEN: [Fp; 16] = [ + Fp::from_raw_unchecked([ + 0xeb6c_359d_47e5_2b1c, + 0x18ef_5f8a_1063_4d60, + 0xddfa_71a0_889d_5b7e, + 0x723e_71dc_c5fc_1323, + 0x52f4_5700_b70d_5c69, + 0x0a8b_981e_e476_91f1, + ]), + Fp::from_raw_unchecked([ + 0x616a_3c4f_5535_b9fb, + 0x6f5f_0373_95db_d911, + 0xf25f_4cc5_e35c_65da, + 0x3e50_dffe_a3c6_2658, + 0x6a33_dca5_2356_0776, + 0x0fad_eff7_7b6b_fe3e, + ]), + Fp::from_raw_unchecked([ + 0x2be9_b66d_f470_059c, + 0x24a2_c159_a3d3_6742, + 0x115d_be7a_d10c_2a37, + 0xb663_4a65_2ee5_884d, + 0x04fe_8bb2_b8d8_1af4, + 0x01c2_a7a2_56fe_9c41, + ]), + Fp::from_raw_unchecked([ + 0xf27b_f8ef_3b75_a386, + 0x898b_3674_76c9_073f, + 0x2448_2e6b_8c2f_4e5f, + 0xc8e0_bbd6_fe11_0806, + 0x59b0_c17f_7631_448a, + 0x1103_7cd5_8b3d_bfbd, + ]), + Fp::from_raw_unchecked([ + 0x31c7_912e_a267_eec6, + 0x1dbf_6f1c_5fcd_b700, + 0xd30d_4fe3_ba86_fdb1, + 0x3cae_528f_bee9_a2a4, + 0xb1cc_e69b_6aa9_ad9a, + 0x0443_93bb_632d_94fb, + ]), + Fp::from_raw_unchecked([ + 0xc66e_f6ef_eeb5_c7e8, + 0x9824_c289_dd72_bb55, + 0x71b1_a4d2_f119_981d, + 0x104f_c1aa_fb09_19cc, + 0x0e49_df01_d942_a628, + 0x096c_3a09_7732_72d4, + ]), + Fp::from_raw_unchecked([ + 0x9abc_11eb_5fad_eff4, + 0x32dc_a50a_8857_28f0, + 0xfb1f_a372_1569_734c, + 0xc4b7_6271_ea65_06b3, + 0xd466_a755_99ce_728e, + 0x0c81_d464_5f4c_b6ed, + ]), + Fp::from_raw_unchecked([ + 0x4199_f10e_5b8b_e45b, + 0xda64_e495_b1e8_7930, + 0xcb35_3efe_9b33_e4ff, + 0x9e9e_fb24_aa64_24c6, + 0xf08d_3368_0a23_7465, + 0x0d33_7802_3e4c_7406, + ]), + Fp::from_raw_unchecked([ + 0x7eb4_ae92_ec74_d3a5, + 0xc341_b4aa_9fac_3497, + 0x5be6_0389_9e90_7687, + 0x03bf_d9cc_a75c_bdeb, + 0x564c_2935_a96b_fa93, + 0x0ef3_c333_71e2_fdb5, + ]), + Fp::from_raw_unchecked([ + 0x7ee9_1fd4_49f6_ac2e, + 0xe5d5_bd5c_b935_7a30, + 0x773a_8ca5_196b_1380, + 0xd0fd_a172_174e_d023, + 0x6cb9_5e0f_a776_aead, + 0x0d22_d5a4_0cec_7cff, + ]), + Fp::from_raw_unchecked([ + 0xf727_e092_85fd_8519, + 0xdc9d_55a8_3017_897b, + 0x7549_d8bd_0578_94ae, + 0x1784_1961_3d90_d8f8, + 0xfce9_5ebd_eb5b_490a, + 0x0467_ffae_f23f_c49e, + ]), + Fp::from_raw_unchecked([ + 0xc176_9e6a_7c38_5f1b, + 0x79bc_930d_eac0_1c03, + 0x5461_c75a_23ed_e3b5, + 0x6e20_829e_5c23_0c45, + 0x828e_0f1e_772a_53cd, + 0x116a_efa7_4912_7bff, + ]), + Fp::from_raw_unchecked([ + 0x101c_10bf_2744_c10a, + 0xbbf1_8d05_3a6a_3154, + 0xa0ec_f39e_f026_f602, + 0xfc00_9d49_96dc_5153, + 0xb900_0209_d5bd_08d3, + 0x189e_5fe4_470c_d73c, + ]), + Fp::from_raw_unchecked([ + 0x7ebd_546c_a157_5ed2, + 0xe47d_5a98_1d08_1b55, + 0x57b2_b625_b6d4_ca21, + 0xb0a1_ba04_2285_20cc, + 0x9873_8983_c210_7ff3, + 0x13dd_dbc4_799d_81d6, + ]), + Fp::from_raw_unchecked([ + 0x0931_9f2e_3983_4935, + 0x039e_952c_bdb0_5c21, + 0x55ba_77a9_a2f7_6493, + 0xfd04_e3df_c608_6467, + 0xfb95_832e_7d78_742e, + 0x0ef9_c24e_ccaf_5e0e, + ]), + Fp::from_raw_unchecked([ + 0x7609_0000_0002_fffd, + 0xebf4_000b_c40c_0002, + 0x5f48_9857_53c7_58ba, + 0x77ce_5853_7052_5745, + 0x5c07_1a97_a256_ec6d, + 0x15f6_5ec3_fa80_e493, + ]), +]; + +const SSWU_ELLP_A: Fp = Fp::from_raw_unchecked([ + 0x2f65_aa0e_9af5_aa51, + 0x8646_4c2d_1e84_16c3, + 0xb85c_e591_b7bd_31e2, + 0x27e1_1c91_b5f2_4e7c, + 0x2837_6eda_6bfc_1835, + 0x1554_55c3_e507_1d85, +]); + +const SSWU_ELLP_B: Fp = Fp::from_raw_unchecked([ + 0xfb99_6971_fe22_a1e0, + 0x9aa9_3eb3_5b74_2d6f, + 0x8c47_6013_de99_c5c4, + 0x873e_27c3_a221_e571, + 0xca72_b5e4_5a52_d888, + 0x0682_4061_418a_386b, +]); + +const SSWU_XI: Fp = Fp::from_raw_unchecked([ + 0x886c_0000_0023_ffdc, + 0x0f70_008d_3090_001d, + 0x7767_2417_ed58_28c3, + 0x9dac_23e9_43dc_1740, + 0x5055_3f1b_9c13_1521, + 0x078c_712f_be0a_b6e8, +]); + +const SQRT_M_XI_CUBED: Fp = Fp::from_raw_unchecked([ + 0x43b5_71ca_d321_5f1f, + 0xccb4_60ef_1c70_2dc2, + 0x742d_884f_4f97_100b, + 0xdb2c_3e32_38a3_382b, + 0xe40f_3fa1_3fce_8f88, + 0x0073_a2af_9892_a2ff, +]); + +impl HashToField for Fp { + // ceil(log2(p)) = 381, m = 1, k = 128. + type InputLength = U64; + + fn from_okm(okm: &GenericArray) -> Fp { + const F_2_256: Fp = Fp::from_raw_unchecked([ + 0x075b_3cd7_c5ce_820f, + 0x3ec6_ba62_1c3e_db0b, + 0x168a_13d8_2bff_6bce, + 0x8766_3c4b_f8c4_49d2, + 0x15f3_4c83_ddc8_d830, + 0x0f96_28b4_9caa_2e85, + ]); + + let mut bs = [0u8; 48]; + bs[16..].copy_from_slice(&okm[..32]); + let db = Fp::from_bytes(&bs).unwrap(); + + bs[16..].copy_from_slice(&okm[32..]); + let da = Fp::from_bytes(&bs).unwrap(); + + db * F_2_256 + da + } +} + +impl Sgn0 for Fp { + fn sgn0(&self) -> Choice { + // Turn into canonical form by computing + // (a.R) / R = a + let tmp = Fp::montgomery_reduce( + self.0[0], self.0[1], self.0[2], self.0[3], self.0[4], self.0[5], 0, 0, 0, 0, 0, 0, + ); + Choice::from((tmp.0[0] & 1) as u8) + } +} + +/// Maps an element of [`Fp`] to a point on iso-G1. +/// +/// Implements [section 6.6.2 of `draft-irtf-cfrg-hash-to-curve-12`][sswu]. +/// +/// [sswu]: https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-12#section-6.6.2 +fn map_to_curve_simple_swu(u: &Fp) -> G1Projective { + let usq = u.square(); + let xi_usq = SSWU_XI * usq; + let xisq_u4 = xi_usq.square(); + let nd_common = xisq_u4 + xi_usq; // XI^2 * u^4 + XI * u^2 + let x_den = SSWU_ELLP_A * Fp::conditional_select(&(-nd_common), &SSWU_XI, nd_common.is_zero()); + let x0_num = SSWU_ELLP_B * (Fp::one() + nd_common); // B * (1 + (XI^2 * u^4 + XI * u^2)) + + // compute g(x0(u)) + let x_densq = x_den.square(); + let gx_den = x_densq * x_den; + // x0_num^3 + A * x0_num * x_den^2 + B * x_den^3 + let gx0_num = (x0_num.square() + SSWU_ELLP_A * x_densq) * x0_num + SSWU_ELLP_B * gx_den; + + // compute g(X0(u)) ^ ((p - 3) // 4) + let sqrt_candidate = { + let u_v = gx0_num * gx_den; // u*v + let vsq = gx_den.square(); // v^2 + u_v * chain_pm3div4(&(u_v * vsq)) // u v (u v^3) ^ ((p - 3) // 4) + }; + + let gx0_square = (sqrt_candidate.square() * gx_den).ct_eq(&gx0_num); // g(x0) is square + let x1_num = x0_num * xi_usq; + // sqrt(-XI**3) * u^3 g(x0) ^ ((p - 3) // 4) + let y1 = SQRT_M_XI_CUBED * usq * u * sqrt_candidate; + + let x_num = Fp::conditional_select(&x1_num, &x0_num, gx0_square); + let mut y = Fp::conditional_select(&y1, &sqrt_candidate, gx0_square); + // ensure sign of y and sign of u agree + y.conditional_negate(y.sgn0() ^ u.sgn0()); + + G1Projective { + x: x_num, + y: y * x_den, + z: x_den, + } +} + +/// Maps an iso-G1 point to a G1 point. +fn iso_map(u: &G1Projective) -> G1Projective { + const COEFFS: [&[Fp]; 4] = [&ISO11_XNUM, &ISO11_XDEN, &ISO11_YNUM, &ISO11_YDEN]; + + // unpack input point + let G1Projective { x, y, z } = *u; + + // xnum, xden, ynum, yden + let mut mapvals = [Fp::zero(); 4]; + + // pre-compute powers of z + let zpows = { + let mut zpows = [Fp::zero(); 15]; + zpows[0] = z; + for idx in 1..zpows.len() { + zpows[idx] = zpows[idx - 1] * z; + } + zpows + }; + + // compute map value by Horner's rule + for idx in 0..4 { + let coeff = COEFFS[idx]; + let clast = coeff.len() - 1; + mapvals[idx] = coeff[clast]; + for jdx in 0..clast { + mapvals[idx] = mapvals[idx] * x + zpows[jdx] * coeff[clast - 1 - jdx]; + } + } + + // x denominator is order 1 less than x numerator, so we need an extra factor of z + mapvals[1] *= z; + + // multiply result of Y map by the y-coord, y / z + mapvals[2] *= y; + mapvals[3] *= z; + + G1Projective { + x: mapvals[0] * mapvals[3], // xnum * yden, + y: mapvals[2] * mapvals[1], // ynum * xden, + z: mapvals[1] * mapvals[3], // xden * yden + } +} + +impl MapToCurve for G1Projective { + type Field = Fp; + + fn map_to_curve(u: &Fp) -> G1Projective { + let pt = map_to_curve_simple_swu(u); + iso_map(&pt) + } + + fn clear_h(&self) -> Self { + self.clear_cofactor() + } +} + +#[cfg(test)] +fn check_g1_prime(pt: &G1Projective) -> bool { + // (X : Y : Z)==(X/Z, Y/Z) is on E': y^2 = x^3 + A * x + B. + // y^2 z = (x^3) + A (x z^2) + B z^3 + let zsq = pt.z.square(); + (pt.y.square() * pt.z) + == (pt.x.square() * pt.x + SSWU_ELLP_A * pt.x * zsq + SSWU_ELLP_B * zsq * pt.z) +} + +#[test] +fn test_simple_swu_expected() { + // exceptional case: zero + let p = map_to_curve_simple_swu(&Fp::zero()); + let G1Projective { x, y, z } = &p; + let xo = Fp::from_raw_unchecked([ + 0xfb99_6971_fe22_a1e0, + 0x9aa9_3eb3_5b74_2d6f, + 0x8c47_6013_de99_c5c4, + 0x873e_27c3_a221_e571, + 0xca72_b5e4_5a52_d888, + 0x0682_4061_418a_386b, + ]); + let yo = Fp::from_raw_unchecked([ + 0xfd6f_ced8_7a7f_11a3, + 0x9a6b_314b_03c8_db31, + 0x41f8_5416_e0ea_b593, + 0xfeeb_089f_7e6e_c4d7, + 0x85a1_34c3_7ed1_278f, + 0x0575_c525_bb9f_74bb, + ]); + let zo = Fp::from_raw_unchecked([ + 0x7f67_4ea0_a891_5178, + 0xb0f9_45fc_13b8_fa65, + 0x4b46_759a_38e8_7d76, + 0x2e7a_9296_41bb_b6a1, + 0x1668_ddfa_462b_f6b6, + 0x0096_0e2e_d1cf_294c, + ]); + assert_eq!(x, &xo); + assert_eq!(y, &yo); + assert_eq!(z, &zo); + assert!(check_g1_prime(&p)); + + // exceptional case: sqrt(-1/XI) (positive) + let excp = Fp::from_raw_unchecked([ + 0x00f3_d047_7e91_edbf, + 0x08d6_621e_4ca8_dc69, + 0xb9cf_7927_b19b_9726, + 0xba13_3c99_6caf_a2ec, + 0xed2a_5ccd_5ca7_bb68, + 0x19cb_022f_8ee9_d73b, + ]); + let p = map_to_curve_simple_swu(&excp); + let G1Projective { x, y, z } = &p; + assert_eq!(x, &xo); + assert_eq!(y, &yo); + assert_eq!(z, &zo); + assert!(check_g1_prime(&p)); + + // exceptional case: sqrt(-1/XI) (negative) + let excp = Fp::from_raw_unchecked([ + 0xb90b_2fb8_816d_bcec, + 0x15d5_9de0_64ab_2396, + 0xad61_5979_4515_5efe, + 0xaa64_0eeb_86d5_6fd2, + 0x5df1_4ae8_e6a3_f16e, + 0x0036_0fba_aa96_0f5e, + ]); + let p = map_to_curve_simple_swu(&excp); + let G1Projective { x, y, z } = &p; + let myo = -yo; + assert_eq!(x, &xo); + assert_eq!(y, &myo); + assert_eq!(z, &zo); + assert!(check_g1_prime(&p)); + + let u = Fp::from_raw_unchecked([ + 0xa618_fa19_f7e2_eadc, + 0x93c7_f1fc_876b_a245, + 0xe2ed_4cc4_7b5c_0ae0, + 0xd49e_fa74_e4a8_d000, + 0xa0b2_3ba6_92b5_431c, + 0x0d15_51f2_d7d8_d193, + ]); + let xo = Fp::from_raw_unchecked([ + 0x2197_ca55_fab3_ba48, + 0x591d_eb39_f434_949a, + 0xf9df_7fb4_f1fa_6a08, + 0x59e3_c16a_9dfa_8fa5, + 0xe592_9b19_4aad_5f7a, + 0x130a_46a4_c61b_44ed, + ]); + let yo = Fp::from_raw_unchecked([ + 0xf721_5b58_c720_0ad0, + 0x8905_1631_3a4e_66bf, + 0xc903_1acc_8a36_19a8, + 0xea1f_9978_fde3_ffec, + 0x0548_f02d_6cfb_f472, + 0x1693_7557_3529_163f, + ]); + let zo = Fp::from_raw_unchecked([ + 0xf36f_eb2e_1128_ade0, + 0x42e2_2214_250b_cd94, + 0xb94f_6ba2_dddf_62d6, + 0xf56d_4392_782b_f0a2, + 0xb2d7_ce1e_c263_09e7, + 0x182b_57ed_6b99_f0a1, + ]); + let p = map_to_curve_simple_swu(&u); + let G1Projective { x, y, z } = &p; + assert_eq!(x, &xo); + assert_eq!(y, &yo); + assert_eq!(z, &zo); + assert!(check_g1_prime(&p)); +} + +#[test] +fn test_osswu_semirandom() { + use rand_core::SeedableRng; + let mut rng = rand_xorshift::XorShiftRng::from_seed([ + 0x59, 0x62, 0xbe, 0x5d, 0x76, 0x3d, 0x31, 0x8d, 0x17, 0xdb, 0x37, 0x32, 0x54, 0x06, 0xbc, + 0xe5, + ]); + for _ in 0..32 { + let input = Fp::random(&mut rng); + let p = map_to_curve_simple_swu(&input); + assert!(check_g1_prime(&p)); + + let p_iso = iso_map(&p); + assert!(bool::from(p_iso.is_on_curve())); + } +} + +// test vectors from the draft 10 RFC +#[test] +fn test_encode_to_curve_10() { + use crate::{ + g1::G1Affine, + hash_to_curve::{ExpandMsgXmd, HashToCurve}, + }; + use std::string::{String, ToString}; + + struct TestCase { + msg: &'static [u8], + expected: [&'static str; 2], + } + impl TestCase { + fn expected(&self) -> String { + self.expected[0].to_string() + self.expected[1] + } + } + + const DOMAIN: &[u8] = b"QUUX-V01-CS02-with-BLS12381G1_XMD:SHA-256_SSWU_NU_"; + + let cases = vec![ + TestCase { + msg: b"", + expected: [ + "184bb665c37ff561a89ec2122dd343f20e0f4cbcaec84e3c3052ea81d1834e192c426074b02ed3dca4e7676ce4ce48ba", + "04407b8d35af4dacc809927071fc0405218f1401a6d15af775810e4e460064bcc9468beeba82fdc751be70476c888bf3", + ], + }, + TestCase { + msg: b"abc", + expected: [ + "009769f3ab59bfd551d53a5f846b9984c59b97d6842b20a2c565baa167945e3d026a3755b6345df8ec7e6acb6868ae6d", + "1532c00cf61aa3d0ce3e5aa20c3b531a2abd2c770a790a2613818303c6b830ffc0ecf6c357af3317b9575c567f11cd2c", + ], + }, + TestCase { + msg: b"abcdef0123456789", + expected: [ + "1974dbb8e6b5d20b84df7e625e2fbfecb2cdb5f77d5eae5fb2955e5ce7313cae8364bc2fff520a6c25619739c6bdcb6a", + "15f9897e11c6441eaa676de141c8d83c37aab8667173cbe1dfd6de74d11861b961dccebcd9d289ac633455dfcc7013a3", + ] + }, + TestCase { + msg: b"q128_qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ + qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ + qqqqqqqqqqqqqqqqqqqqqqqqq", + expected: [ + "0a7a047c4a8397b3446450642c2ac64d7239b61872c9ae7a59707a8f4f950f101e766afe58223b3bff3a19a7f754027c", + "1383aebba1e4327ccff7cf9912bda0dbc77de048b71ef8c8a81111d71dc33c5e3aa6edee9cf6f5fe525d50cc50b77cc9", + ] + }, + TestCase { + msg: b"a512_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + expected: [ + "0e7a16a975904f131682edbb03d9560d3e48214c9986bd50417a77108d13dc957500edf96462a3d01e62dc6cd468ef11", + "0ae89e677711d05c30a48d6d75e76ca9fb70fe06c6dd6ff988683d89ccde29ac7d46c53bb97a59b1901abf1db66052db", + ] + } + ]; + + for case in cases { + let g = >>::encode_to_curve( + &case.msg, DOMAIN, + ); + let aff = G1Affine::from(g); + let g_uncompressed = aff.to_uncompressed(); + + assert_eq!(case.expected(), hex::encode(&g_uncompressed[..])); + } +} + +// test vectors from the draft 10 RFC +#[test] +fn test_hash_to_curve_10() { + use crate::{ + g1::G1Affine, + hash_to_curve::{ExpandMsgXmd, HashToCurve}, + }; + use std::string::{String, ToString}; + + struct TestCase { + msg: &'static [u8], + expected: [&'static str; 2], + } + impl TestCase { + fn expected(&self) -> String { + self.expected[0].to_string() + self.expected[1] + } + } + + const DOMAIN: &[u8] = b"QUUX-V01-CS02-with-BLS12381G1_XMD:SHA-256_SSWU_RO_"; + + let cases = vec![ + TestCase { + msg: b"", + expected: [ + "052926add2207b76ca4fa57a8734416c8dc95e24501772c814278700eed6d1e4e8cf62d9c09db0fac349612b759e79a1", + "08ba738453bfed09cb546dbb0783dbb3a5f1f566ed67bb6be0e8c67e2e81a4cc68ee29813bb7994998f3eae0c9c6a265", + ], + }, + TestCase { + msg: b"abc", + expected: [ + "03567bc5ef9c690c2ab2ecdf6a96ef1c139cc0b2f284dca0a9a7943388a49a3aee664ba5379a7655d3c68900be2f6903", + "0b9c15f3fe6e5cf4211f346271d7b01c8f3b28be689c8429c85b67af215533311f0b8dfaaa154fa6b88176c229f2885d" + ], + }, + TestCase { + msg: b"abcdef0123456789", + expected: [ + "11e0b079dea29a68f0383ee94fed1b940995272407e3bb916bbf268c263ddd57a6a27200a784cbc248e84f357ce82d98", + "03a87ae2caf14e8ee52e51fa2ed8eefe80f02457004ba4d486d6aa1f517c0889501dc7413753f9599b099ebcbbd2d709" + ] + }, + TestCase { + msg: b"q128_qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ + qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ + qqqqqqqqqqqqqqqqqqqqqqqqq", + expected: [ + "15f68eaa693b95ccb85215dc65fa81038d69629f70aeee0d0f677cf22285e7bf58d7cb86eefe8f2e9bc3f8cb84fac488", + "1807a1d50c29f430b8cafc4f8638dfeeadf51211e1602a5f184443076715f91bb90a48ba1e370edce6ae1062f5e6dd38" + ] + }, + TestCase { + msg: b"a512_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + expected: [ + "082aabae8b7dedb0e78aeb619ad3bfd9277a2f77ba7fad20ef6aabdc6c31d19ba5a6d12283553294c1825c4b3ca2dcfe", + "05b84ae5a942248eea39e1d91030458c40153f3b654ab7872d779ad1e942856a20c438e8d99bc8abfbf74729ce1f7ac8" + ] + } + ]; + + for case in cases { + let g = >>::hash_to_curve( + &case.msg, DOMAIN, + ); + let g_uncompressed = G1Affine::from(g).to_uncompressed(); + + assert_eq!(case.expected(), hex::encode(&g_uncompressed[..])); + } +} + +#[cfg(test)] +// p-1 / 2 +pub const P_M1_OVER2: Fp = Fp::from_raw_unchecked([ + 0xa1fa_ffff_fffe_5557, + 0x995b_fff9_76a3_fffe, + 0x03f4_1d24_d174_ceb4, + 0xf654_7998_c199_5dbd, + 0x778a_468f_507a_6034, + 0x0205_5993_1f7f_8103, +]); + +#[test] +fn test_sgn0() { + assert_eq!(bool::from(Fp::zero().sgn0()), false); + assert_eq!(bool::from(Fp::one().sgn0()), true); + assert_eq!(bool::from((-Fp::one()).sgn0()), false); + assert_eq!(bool::from((-Fp::zero()).sgn0()), false); + assert_eq!(bool::from(P_M1_OVER2.sgn0()), true); + + let p_p1_over2 = P_M1_OVER2 + Fp::one(); + assert_eq!(bool::from(p_p1_over2.sgn0()), false); + + let neg_p_p1_over2 = { + let mut tmp = p_p1_over2; + tmp.conditional_negate(Choice::from(1u8)); + tmp + }; + assert_eq!(neg_p_p1_over2, P_M1_OVER2); +} diff --git a/constantine/bls12_381/src/hash_to_curve/map_g2.rs b/constantine/bls12_381/src/hash_to_curve/map_g2.rs new file mode 100644 index 000000000..b2cdb956d --- /dev/null +++ b/constantine/bls12_381/src/hash_to_curve/map_g2.rs @@ -0,0 +1,900 @@ +//! Implementation of hash-to-curve for the G2 group + +use subtle::{Choice, ConditionallyNegatable, ConditionallySelectable, ConstantTimeEq}; + +use super::chain::chain_p2m9div16; +use super::{HashToField, MapToCurve, Sgn0}; +use crate::generic_array::{ + typenum::{U128, U64}, + GenericArray, +}; +use crate::{fp::Fp, fp2::Fp2, g2::G2Projective}; + +/// Coefficients of the 3-isogeny x map's numerator +const ISO3_XNUM: [Fp2; 4] = [ + Fp2 { + c0: Fp::from_raw_unchecked([ + 0x47f6_71c7_1ce0_5e62, + 0x06dd_5707_1206_393e, + 0x7c80_cd2a_f3fd_71a2, + 0x0481_03ea_9e6c_d062, + 0xc545_16ac_c8d0_37f6, + 0x1380_8f55_0920_ea41, + ]), + c1: Fp::from_raw_unchecked([ + 0x47f6_71c7_1ce0_5e62, + 0x06dd_5707_1206_393e, + 0x7c80_cd2a_f3fd_71a2, + 0x0481_03ea_9e6c_d062, + 0xc545_16ac_c8d0_37f6, + 0x1380_8f55_0920_ea41, + ]), + }, + Fp2 { + c0: Fp::zero(), + c1: Fp::from_raw_unchecked([ + 0x5fe5_5555_554c_71d0, + 0x873f_ffdd_236a_aaa3, + 0x6a6b_4619_b26e_f918, + 0x21c2_8884_0887_4945, + 0x2836_cda7_028c_abc5, + 0x0ac7_3310_a7fd_5abd, + ]), + }, + Fp2 { + c0: Fp::from_raw_unchecked([ + 0x0a0c_5555_5559_71c3, + 0xdb0c_0010_1f9e_aaae, + 0xb1fb_2f94_1d79_7997, + 0xd396_0742_ef41_6e1c, + 0xb700_40e2_c205_56f4, + 0x149d_7861_e581_393b, + ]), + c1: Fp::from_raw_unchecked([ + 0xaff2_aaaa_aaa6_38e8, + 0x439f_ffee_91b5_5551, + 0xb535_a30c_d937_7c8c, + 0x90e1_4442_0443_a4a2, + 0x941b_66d3_8146_55e2, + 0x0563_9988_53fe_ad5e, + ]), + }, + Fp2 { + c0: Fp::from_raw_unchecked([ + 0x40aa_c71c_71c7_25ed, + 0x1909_5555_7a84_e38e, + 0xd817_050a_8f41_abc3, + 0xd864_85d4_c87f_6fb1, + 0x696e_b479_f885_d059, + 0x198e_1a74_3280_02d2, + ]), + c1: Fp::zero(), + }, +]; + +/// Coefficients of the 3-isogeny x map's denominator +const ISO3_XDEN: [Fp2; 3] = [ + Fp2 { + c0: Fp::zero(), + c1: Fp::from_raw_unchecked([ + 0x1f3a_ffff_ff13_ab97, + 0xf25b_fc61_1da3_ff3e, + 0xca37_57cb_3819_b208, + 0x3e64_2736_6f8c_ec18, + 0x0397_7bc8_6095_b089, + 0x04f6_9db1_3f39_a952, + ]), + }, + Fp2 { + c0: Fp::from_raw_unchecked([ + 0x4476_0000_0027_552e, + 0xdcb8_009a_4348_0020, + 0x6f7e_e9ce_4a6e_8b59, + 0xb103_30b7_c0a9_5bc6, + 0x6140_b1fc_fb1e_54b7, + 0x0381_be09_7f0b_b4e1, + ]), + c1: Fp::from_raw_unchecked([ + 0x7588_ffff_ffd8_557d, + 0x41f3_ff64_6e0b_ffdf, + 0xf7b1_e8d2_ac42_6aca, + 0xb374_1acd_32db_b6f8, + 0xe9da_f5b9_482d_581f, + 0x167f_53e0_ba74_31b8, + ]), + }, + Fp2::one(), +]; + +/// Coefficients of the 3-isogeny y map's numerator +const ISO3_YNUM: [Fp2; 4] = [ + Fp2 { + c0: Fp::from_raw_unchecked([ + 0x96d8_f684_bdfc_77be, + 0xb530_e4f4_3b66_d0e2, + 0x184a_88ff_3796_52fd, + 0x57cb_23ec_fae8_04e1, + 0x0fd2_e39e_ada3_eba9, + 0x08c8_055e_31c5_d5c3, + ]), + c1: Fp::from_raw_unchecked([ + 0x96d8_f684_bdfc_77be, + 0xb530_e4f4_3b66_d0e2, + 0x184a_88ff_3796_52fd, + 0x57cb_23ec_fae8_04e1, + 0x0fd2_e39e_ada3_eba9, + 0x08c8_055e_31c5_d5c3, + ]), + }, + Fp2 { + c0: Fp::zero(), + c1: Fp::from_raw_unchecked([ + 0xbf0a_71c7_1c91_b406, + 0x4d6d_55d2_8b76_38fd, + 0x9d82_f98e_5f20_5aee, + 0xa27a_a27b_1d1a_18d5, + 0x02c3_b2b2_d293_8e86, + 0x0c7d_1342_0b09_807f, + ]), + }, + Fp2 { + c0: Fp::from_raw_unchecked([ + 0xd7f9_5555_5553_1c74, + 0x21cf_fff7_48da_aaa8, + 0x5a9a_d186_6c9b_be46, + 0x4870_a221_0221_d251, + 0x4a0d_b369_c0a3_2af1, + 0x02b1_ccc4_29ff_56af, + ]), + c1: Fp::from_raw_unchecked([ + 0xe205_aaaa_aaac_8e37, + 0xfcdc_0007_6879_5556, + 0x0c96_011a_8a15_37dd, + 0x1c06_a963_f163_406e, + 0x010d_f44c_82a8_81e6, + 0x174f_4526_0f80_8feb, + ]), + }, + Fp2 { + c0: Fp::from_raw_unchecked([ + 0xa470_bda1_2f67_f35c, + 0xc0fe_38e2_3327_b425, + 0xc9d3_d0f2_c6f0_678d, + 0x1c55_c993_5b5a_982e, + 0x27f6_c0e2_f074_6764, + 0x117c_5e6e_28aa_9054, + ]), + c1: Fp::zero(), + }, +]; + +/// Coefficients of the 3-isogeny y map's denominator +const ISO3_YDEN: [Fp2; 4] = [ + Fp2 { + c0: Fp::from_raw_unchecked([ + 0x0162_ffff_fa76_5adf, + 0x8f7b_ea48_0083_fb75, + 0x561b_3c22_59e9_3611, + 0x11e1_9fc1_a9c8_75d5, + 0xca71_3efc_0036_7660, + 0x03c6_a03d_41da_1151, + ]), + c1: Fp::from_raw_unchecked([ + 0x0162_ffff_fa76_5adf, + 0x8f7b_ea48_0083_fb75, + 0x561b_3c22_59e9_3611, + 0x11e1_9fc1_a9c8_75d5, + 0xca71_3efc_0036_7660, + 0x03c6_a03d_41da_1151, + ]), + }, + Fp2 { + c0: Fp::zero(), + c1: Fp::from_raw_unchecked([ + 0x5db0_ffff_fd3b_02c5, + 0xd713_f523_58eb_fdba, + 0x5ea6_0761_a84d_161a, + 0xbb2c_75a3_4ea6_c44a, + 0x0ac6_7359_21c1_119b, + 0x0ee3_d913_bdac_fbf6, + ]), + }, + Fp2 { + c0: Fp::from_raw_unchecked([ + 0x66b1_0000_003a_ffc5, + 0xcb14_00e7_64ec_0030, + 0xa73e_5eb5_6fa5_d106, + 0x8984_c913_a0fe_09a9, + 0x11e1_0afb_78ad_7f13, + 0x0542_9d0e_3e91_8f52, + ]), + c1: Fp::from_raw_unchecked([ + 0x534d_ffff_ffc4_aae6, + 0x5397_ff17_4c67_ffcf, + 0xbff2_73eb_870b_251d, + 0xdaf2_8271_5287_0915, + 0x393a_9cba_ca9e_2dc3, + 0x14be_74db_faee_5748, + ]), + }, + Fp2::one(), +]; + +const SSWU_ELLP_A: Fp2 = Fp2 { + c0: Fp::zero(), + c1: Fp::from_raw_unchecked([ + 0xe53a_0000_0313_5242, + 0x0108_0c0f_def8_0285, + 0xe788_9edb_e340_f6bd, + 0x0b51_3751_2631_0601, + 0x02d6_9857_17c7_44ab, + 0x1220_b4e9_79ea_5467, + ]), +}; + +const SSWU_ELLP_B: Fp2 = Fp2 { + c0: Fp::from_raw_unchecked([ + 0x22ea_0000_0cf8_9db2, + 0x6ec8_32df_7138_0aa4, + 0x6e1b_9440_3db5_a66e, + 0x75bf_3c53_a794_73ba, + 0x3dd3_a569_412c_0a34, + 0x125c_db5e_74dc_4fd1, + ]), + c1: Fp::from_raw_unchecked([ + 0x22ea_0000_0cf8_9db2, + 0x6ec8_32df_7138_0aa4, + 0x6e1b_9440_3db5_a66e, + 0x75bf_3c53_a794_73ba, + 0x3dd3_a569_412c_0a34, + 0x125c_db5e_74dc_4fd1, + ]), +}; + +const SSWU_XI: Fp2 = Fp2 { + c0: Fp::from_raw_unchecked([ + 0x87eb_ffff_fff9_555c, + 0x656f_ffe5_da8f_fffa, + 0x0fd0_7493_45d3_3ad2, + 0xd951_e663_0665_76f4, + 0xde29_1a3d_41e9_80d3, + 0x0815_664c_7dfe_040d, + ]), + c1: Fp::from_raw_unchecked([ + 0x43f5_ffff_fffc_aaae, + 0x32b7_fff2_ed47_fffd, + 0x07e8_3a49_a2e9_9d69, + 0xeca8_f331_8332_bb7a, + 0xef14_8d1e_a0f4_c069, + 0x040a_b326_3eff_0206, + ]), +}; + +const SSWU_ETAS: [Fp2; 4] = [ + Fp2 { + c0: Fp::from_raw_unchecked([ + 0x05e5_1466_8ac7_36d2, + 0x9089_b4d6_b84f_3ea5, + 0x603c_384c_224a_8b32, + 0xf325_7909_536a_fea6, + 0x5c5c_dbab_ae65_6d81, + 0x075b_fa08_63c9_87e9, + ]), + c1: Fp::from_raw_unchecked([ + 0x338d_9bfe_0808_7330, + 0x7b8e_48b2_bd83_cefe, + 0x530d_ad5d_306b_5be7, + 0x5a4d_7e8e_6c40_8b6d, + 0x6258_f7a6_232c_ab9b, + 0x0b98_5811_cce1_4db5, + ]), + }, + Fp2 { + c0: Fp::from_raw_unchecked([ + 0x8671_6401_f7f7_377b, + 0xa31d_b74b_f3d0_3101, + 0x1423_2543_c645_9a3c, + 0x0a29_ccf6_8744_8752, + 0xe8c2_b010_201f_013c, + 0x0e68_b9d8_6c9e_98e4, + ]), + c1: Fp::from_raw_unchecked([ + 0x05e5_1466_8ac7_36d2, + 0x9089_b4d6_b84f_3ea5, + 0x603c_384c_224a_8b32, + 0xf325_7909_536a_fea6, + 0x5c5c_dbab_ae65_6d81, + 0x075b_fa08_63c9_87e9, + ]), + }, + Fp2 { + c0: Fp::from_raw_unchecked([ + 0x718f_dad2_4ee1_d90f, + 0xa58c_025b_ed82_76af, + 0x0c3a_1023_0ab7_976f, + 0xf0c5_4df5_c8f2_75e1, + 0x4ec2_478c_28ba_f465, + 0x1129_373a_90c5_08e6, + ]), + c1: Fp::from_raw_unchecked([ + 0x019a_f5f9_80a3_680c, + 0x4ed7_da0e_6606_3afa, + 0x6003_5472_3b5d_9972, + 0x8b2f_958b_20d0_9d72, + 0x0474_938f_02d4_61db, + 0x0dcf_8b9e_0684_ab1c, + ]), + }, + Fp2 { + c0: Fp::from_raw_unchecked([ + 0xb864_0a06_7f5c_429f, + 0xcfd4_25f0_4b4d_c505, + 0x072d_7e2e_bb53_5cb1, + 0xd947_b5f9_d2b4_754d, + 0x46a7_1427_4077_4afb, + 0x0c31_864c_32fb_3b7e, + ]), + c1: Fp::from_raw_unchecked([ + 0x718f_dad2_4ee1_d90f, + 0xa58c_025b_ed82_76af, + 0x0c3a_1023_0ab7_976f, + 0xf0c5_4df5_c8f2_75e1, + 0x4ec2_478c_28ba_f465, + 0x1129_373a_90c5_08e6, + ]), + }, +]; + +const SSWU_RV1: Fp2 = Fp2 { + c0: Fp::from_raw_unchecked([ + 0x7bcf_a7a2_5aa3_0fda, + 0xdc17_dec1_2a92_7e7c, + 0x2f08_8dd8_6b4e_bef1, + 0xd1ca_2087_da74_d4a7, + 0x2da2_5966_96ce_bc1d, + 0x0e2b_7eed_bbfd_87d2, + ]), + c1: Fp::from_raw_unchecked([ + 0x7bcf_a7a2_5aa3_0fda, + 0xdc17_dec1_2a92_7e7c, + 0x2f08_8dd8_6b4e_bef1, + 0xd1ca_2087_da74_d4a7, + 0x2da2_5966_96ce_bc1d, + 0x0e2b_7eed_bbfd_87d2, + ]), +}; + +impl HashToField for Fp2 { + // ceil(log2(p)) = 381, m = 2, k = 128. + type InputLength = U128; + + fn from_okm(okm: &GenericArray) -> Fp2 { + let c0 = ::from_okm(GenericArray::::from_slice(&okm[..64])); + let c1 = ::from_okm(GenericArray::::from_slice(&okm[64..])); + Fp2 { c0, c1 } + } +} + +impl Sgn0 for Fp2 { + fn sgn0(&self) -> Choice { + let sign_0 = self.c0.sgn0(); + let zero_0 = self.c0.is_zero(); + let sign_1 = self.c1.sgn0(); + sign_0 | (zero_0 & sign_1) + } +} + +/// Maps from an [`Fp2]` element to a point on iso-G2. +fn map_to_curve_simple_swu(u: &Fp2) -> G2Projective { + let usq = u.square(); + let xi_usq = SSWU_XI * usq; + let xisq_u4 = xi_usq.square(); + let nd_common = xisq_u4 + xi_usq; // XI^2 * u^4 + XI * u^2 + let x_den = SSWU_ELLP_A * Fp2::conditional_select(&(-nd_common), &SSWU_XI, nd_common.is_zero()); + let x0_num = SSWU_ELLP_B * (Fp2::one() + nd_common); // B * (1 + (XI^2 * u^4 + XI * u^2)) + + // compute g(x0(u)) + let x_densq = x_den.square(); + let gx_den = x_densq * x_den; + // x0_num^3 + A * x0_num * x_den^2 + B * x_den^3 + let gx0_num = (x0_num.square() + SSWU_ELLP_A * x_densq) * x0_num + SSWU_ELLP_B * gx_den; + + // compute g(x0(u)) ^ ((p^2 - 9) // 16) + let sqrt_candidate = { + let vsq = gx_den.square(); // v^2 + let v_3 = vsq * gx_den; // v^3 + let v_4 = vsq.square(); // v^4 + let uv_7 = gx0_num * v_3 * v_4; // u v^7 + let uv_15 = uv_7 * v_4.square(); // u v^15 + uv_7 * chain_p2m9div16(&uv_15) // u v^7 (u v^15) ^ ((p^2 - 9) // 16) + }; + + // set y = sqrt_candidate * Fp2::one(), check candidate against other roots of unity + let mut y = sqrt_candidate; + // check Fp2(0, 1) + let tmp = Fp2 { + c0: -sqrt_candidate.c1, + c1: sqrt_candidate.c0, + }; + y.conditional_assign(&tmp, (tmp.square() * gx_den).ct_eq(&gx0_num)); + // check Fp2(RV1, RV1) + let tmp = sqrt_candidate * SSWU_RV1; + y.conditional_assign(&tmp, (tmp.square() * gx_den).ct_eq(&gx0_num)); + // check Fp2(RV1, -RV1) + let tmp = Fp2 { + c0: tmp.c1, + c1: -tmp.c0, + }; + y.conditional_assign(&tmp, (tmp.square() * gx_den).ct_eq(&gx0_num)); + + // compute g(x1(u)) = g(x0(u)) * XI^3 * u^6 + let gx1_num = gx0_num * xi_usq * xisq_u4; + // compute g(x1(u)) * u^3 + let sqrt_candidate = sqrt_candidate * usq * u; + let mut eta_found = Choice::from(0u8); + for eta in &SSWU_ETAS[..] { + let tmp = sqrt_candidate * eta; + let found = (tmp.square() * gx_den).ct_eq(&gx1_num); + y.conditional_assign(&tmp, found); + eta_found |= found; + } + + let x_num = Fp2::conditional_select(&x0_num, &(x0_num * xi_usq), eta_found); + // ensure sign of y and sign of u agree + y.conditional_negate(u.sgn0() ^ y.sgn0()); + + G2Projective { + x: x_num, + y: y * x_den, + z: x_den, + } +} + +/// Maps from an iso-G2 point to a G2 point. +fn iso_map(u: &G2Projective) -> G2Projective { + const COEFFS: [&[Fp2]; 4] = [&ISO3_XNUM, &ISO3_XDEN, &ISO3_YNUM, &ISO3_YDEN]; + + // unpack input point + let G2Projective { x, y, z } = *u; + + // xnum, xden, ynum, yden + let mut mapvals = [Fp2::zero(); 4]; + + // compute powers of z + let zsq = z.square(); + let zpows = [z, zsq, zsq * z]; + + // compute map value by Horner's rule + for idx in 0..4 { + let coeff = COEFFS[idx]; + let clast = coeff.len() - 1; + mapvals[idx] = coeff[clast]; + for jdx in 0..clast { + mapvals[idx] = mapvals[idx] * x + zpows[jdx] * coeff[clast - 1 - jdx]; + } + } + + // x denominator is order 1 less than x numerator, so we need an extra factor of z + mapvals[1] *= z; + + // multiply result of Y map by the y-coord, y / z + mapvals[2] *= y; + mapvals[3] *= z; + + G2Projective { + x: mapvals[0] * mapvals[3], // xnum * yden, + y: mapvals[2] * mapvals[1], // ynum * xden, + z: mapvals[1] * mapvals[3], // xden * yden + } +} + +impl MapToCurve for G2Projective { + type Field = Fp2; + + fn map_to_curve(u: &Fp2) -> G2Projective { + let pt = map_to_curve_simple_swu(u); + iso_map(&pt) + } + + fn clear_h(&self) -> Self { + self.clear_cofactor() + } +} + +#[cfg(test)] +fn check_g2_prime(pt: &G2Projective) -> bool { + // (X : Y : Z)==(X/Z, Y/Z) is on E': y^2 = x^3 + A * x + B. + // y^2 z = (x^3) + A (x z^2) + B z^3 + let zsq = pt.z.square(); + (pt.y.square() * pt.z) + == (pt.x.square() * pt.x + SSWU_ELLP_A * pt.x * zsq + SSWU_ELLP_B * zsq * pt.z) +} + +#[test] +fn test_osswu_semirandom() { + use rand_core::SeedableRng; + let mut rng = rand_xorshift::XorShiftRng::from_seed([ + 0x59, 0x62, 0xbe, 0x5d, 0x76, 0x3d, 0x31, 0x8d, 0x17, 0xdb, 0x37, 0x32, 0x54, 0x06, 0xbc, + 0xe5, + ]); + for _ in 0..32 { + let input = Fp2::random(&mut rng); + let p = map_to_curve_simple_swu(&input); + assert!(check_g2_prime(&p)); + + let p_iso = iso_map(&p); + assert!(bool::from(p_iso.is_on_curve())); + } +} + +// test vectors from the draft 10 RFC +#[test] +fn test_encode_to_curve_10() { + use crate::{ + g2::G2Affine, + hash_to_curve::{ExpandMsgXmd, HashToCurve}, + }; + use std::string::{String, ToString}; + + struct TestCase { + msg: &'static [u8], + expected: [&'static str; 4], + } + impl TestCase { + fn expected(&self) -> String { + self.expected[0].to_string() + self.expected[1] + self.expected[2] + self.expected[3] + } + } + + const DOMAIN: &[u8] = b"QUUX-V01-CS02-with-BLS12381G2_XMD:SHA-256_SSWU_NU_"; + + let cases = vec![ + TestCase { + msg: b"", + expected: [ + "126b855e9e69b1f691f816e48ac6977664d24d99f8724868a184186469ddfd4617367e94527d4b74fc86413483afb35b", + "00e7f4568a82b4b7dc1f14c6aaa055edf51502319c723c4dc2688c7fe5944c213f510328082396515734b6612c4e7bb7", + "1498aadcf7ae2b345243e281ae076df6de84455d766ab6fcdaad71fab60abb2e8b980a440043cd305db09d283c895e3d", + "0caead0fd7b6176c01436833c79d305c78be307da5f6af6c133c47311def6ff1e0babf57a0fb5539fce7ee12407b0a42", + ], + }, + TestCase { + msg: b"abc", + expected: [ + "0296238ea82c6d4adb3c838ee3cb2346049c90b96d602d7bb1b469b905c9228be25c627bffee872def773d5b2a2eb57d", + "108ed59fd9fae381abfd1d6bce2fd2fa220990f0f837fa30e0f27914ed6e1454db0d1ee957b219f61da6ff8be0d6441f", + "153606c417e59fb331b7ae6bce4fbf7c5190c33ce9402b5ebe2b70e44fca614f3f1382a3625ed5493843d0b0a652fc3f", + "033f90f6057aadacae7963b0a0b379dd46750c1c94a6357c99b65f63b79e321ff50fe3053330911c56b6ceea08fee656", + ], + }, + TestCase { + msg: b"abcdef0123456789", + expected: [ + "0da75be60fb6aa0e9e3143e40c42796edf15685cafe0279afd2a67c3dff1c82341f17effd402e4f1af240ea90f4b659b", + "038af300ef34c7759a6caaa4e69363cafeed218a1f207e93b2c70d91a1263d375d6730bd6b6509dcac3ba5b567e85bf3", + "0492f4fed741b073e5a82580f7c663f9b79e036b70ab3e51162359cec4e77c78086fe879b65ca7a47d34374c8315ac5e", + "19b148cbdf163cf0894f29660d2e7bfb2b68e37d54cc83fd4e6e62c020eaa48709302ef8e746736c0e19342cc1ce3df4", + ] + }, + TestCase { + msg: b"q128_qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ + qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ + qqqqqqqqqqqqqqqqqqqqqqqqq", + expected: [ + "12c8c05c1d5fc7bfa847f4d7d81e294e66b9a78bc9953990c358945e1f042eedafce608b67fdd3ab0cb2e6e263b9b1ad", + "0c5ae723be00e6c3f0efe184fdc0702b64588fe77dda152ab13099a3bacd3876767fa7bbad6d6fd90b3642e902b208f9", + "11c624c56dbe154d759d021eec60fab3d8b852395a89de497e48504366feedd4662d023af447d66926a28076813dd646", + "04e77ddb3ede41b5ec4396b7421dd916efc68a358a0d7425bddd253547f2fb4830522358491827265dfc5bcc1928a569", + ] + }, + TestCase { + msg: b"a512_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + expected: [ + "1565c2f625032d232f13121d3cfb476f45275c303a037faa255f9da62000c2c864ea881e2bcddd111edc4a3c0da3e88d", + "0ea4e7c33d43e17cc516a72f76437c4bf81d8f4eac69ac355d3bf9b71b8138d55dc10fd458be115afa798b55dac34be1", + "0f8991d2a1ad662e7b6f58ab787947f1fa607fce12dde171bc17903b012091b657e15333e11701edcf5b63ba2a561247", + "043b6f5fe4e52c839148dc66f2b3751e69a0f6ebb3d056d6465d50d4108543ecd956e10fa1640dfd9bc0030cc2558d28", + ] + } + ]; + + for case in cases { + let g = >>::encode_to_curve( + &case.msg, DOMAIN, + ); + let g_uncompressed = G2Affine::from(g).to_uncompressed(); + + assert_eq!(case.expected(), hex::encode(&g_uncompressed[..])); + } +} + +// test vectors from the draft 10 RFC +#[test] +fn test_hash_to_curve_10() { + use crate::{ + g2::G2Affine, + hash_to_curve::{ExpandMsgXmd, HashToCurve}, + }; + use std::string::{String, ToString}; + + struct TestCase { + msg: &'static [u8], + expected: [&'static str; 4], + } + impl TestCase { + fn expected(&self) -> String { + self.expected[0].to_string() + self.expected[1] + self.expected[2] + self.expected[3] + } + } + + const DOMAIN: &[u8] = b"QUUX-V01-CS02-with-BLS12381G2_XMD:SHA-256_SSWU_RO_"; + + let cases = vec![ + TestCase { + msg: b"", + expected: [ + "05cb8437535e20ecffaef7752baddf98034139c38452458baeefab379ba13dff5bf5dd71b72418717047f5b0f37da03d", + "0141ebfbdca40eb85b87142e130ab689c673cf60f1a3e98d69335266f30d9b8d4ac44c1038e9dcdd5393faf5c41fb78a", + "12424ac32561493f3fe3c260708a12b7c620e7be00099a974e259ddc7d1f6395c3c811cdd19f1e8dbf3e9ecfdcbab8d6", + "0503921d7f6a12805e72940b963c0cf3471c7b2a524950ca195d11062ee75ec076daf2d4bc358c4b190c0c98064fdd92", + ], + }, + TestCase { + msg: b"abc", + expected: [ + "139cddbccdc5e91b9623efd38c49f81a6f83f175e80b06fc374de9eb4b41dfe4ca3a230ed250fbe3a2acf73a41177fd8", + "02c2d18e033b960562aae3cab37a27ce00d80ccd5ba4b7fe0e7a210245129dbec7780ccc7954725f4168aff2787776e6", + "00aa65dae3c8d732d10ecd2c50f8a1baf3001578f71c694e03866e9f3d49ac1e1ce70dd94a733534f106d4cec0eddd16", + "1787327b68159716a37440985269cf584bcb1e621d3a7202be6ea05c4cfe244aeb197642555a0645fb87bf7466b2ba48", + ], + }, + TestCase { + msg: b"abcdef0123456789", + expected: [ + "190d119345b94fbd15497bcba94ecf7db2cbfd1e1fe7da034d26cbba169fb3968288b3fafb265f9ebd380512a71c3f2c", + "121982811d2491fde9ba7ed31ef9ca474f0e1501297f68c298e9f4c0028add35aea8bb83d53c08cfc007c1e005723cd0", + "0bb5e7572275c567462d91807de765611490205a941a5a6af3b1691bfe596c31225d3aabdf15faff860cb4ef17c7c3be", + "05571a0f8d3c08d094576981f4a3b8eda0a8e771fcdcc8ecceaf1356a6acf17574518acb506e435b639353c2e14827c8", + ] + }, + TestCase { + msg: b"q128_qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ + qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ + qqqqqqqqqqqqqqqqqqqqqqqqq", + expected: [ + "0934aba516a52d8ae479939a91998299c76d39cc0c035cd18813bec433f587e2d7a4fef038260eef0cef4d02aae3eb91", + "19a84dd7248a1066f737cc34502ee5555bd3c19f2ecdb3c7d9e24dc65d4e25e50d83f0f77105e955d78f4762d33c17da", + "09bcccfa036b4847c9950780733633f13619994394c23ff0b32fa6b795844f4a0673e20282d07bc69641cee04f5e5662", + "14f81cd421617428bc3b9fe25afbb751d934a00493524bc4e065635b0555084dd54679df1536101b2c979c0152d09192", + ] + }, + TestCase { + msg: b"a512_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + expected: [ + "11fca2ff525572795a801eed17eb12785887c7b63fb77a42be46ce4a34131d71f7a73e95fee3f812aea3de78b4d01569", + "01a6ba2f9a11fa5598b2d8ace0fbe0a0eacb65deceb476fbbcb64fd24557c2f4b18ecfc5663e54ae16a84f5ab7f62534", + "03a47f8e6d1763ba0cad63d6114c0accbef65707825a511b251a660a9b3994249ae4e63fac38b23da0c398689ee2ab52", + "0b6798718c8aed24bc19cb27f866f1c9effcdbf92397ad6448b5c9db90d2b9da6cbabf48adc1adf59a1a28344e79d57e", + ] + } + ]; + + for case in cases { + let g = >>::hash_to_curve( + &case.msg, DOMAIN, + ); + let g_uncompressed = G2Affine::from(g).to_uncompressed(); + + assert_eq!(case.expected(), hex::encode(&g_uncompressed[..])); + } +} + +#[test] +fn test_sgn0() { + use super::map_g1::P_M1_OVER2; + + assert_eq!(bool::from(Fp2::zero().sgn0()), false); + assert_eq!(bool::from(Fp2::one().sgn0()), true); + assert_eq!( + bool::from( + Fp2 { + c0: P_M1_OVER2, + c1: Fp::zero() + } + .sgn0() + ), + true + ); + assert_eq!( + bool::from( + Fp2 { + c0: P_M1_OVER2, + c1: Fp::one() + } + .sgn0() + ), + true + ); + assert_eq!( + bool::from( + Fp2 { + c0: Fp::zero(), + c1: P_M1_OVER2, + } + .sgn0() + ), + true + ); + assert_eq!( + bool::from( + Fp2 { + c0: Fp::one(), + c1: P_M1_OVER2, + } + .sgn0() + ), + true + ); + + let p_p1_over2 = P_M1_OVER2 + Fp::one(); + assert_eq!( + bool::from( + Fp2 { + c0: p_p1_over2, + c1: Fp::zero() + } + .sgn0() + ), + false + ); + assert_eq!( + bool::from( + Fp2 { + c0: p_p1_over2, + c1: Fp::one() + } + .sgn0() + ), + false + ); + assert_eq!( + bool::from( + Fp2 { + c0: Fp::zero(), + c1: p_p1_over2, + } + .sgn0() + ), + false + ); + assert_eq!( + bool::from( + Fp2 { + c0: Fp::one(), + c1: p_p1_over2, + } + .sgn0() + ), + true + ); + + assert_eq!( + bool::from( + Fp2 { + c0: P_M1_OVER2, + c1: -Fp::one() + } + .sgn0() + ), + true + ); + assert_eq!( + bool::from( + Fp2 { + c0: p_p1_over2, + c1: -Fp::one() + } + .sgn0() + ), + false + ); + assert_eq!( + bool::from( + Fp2 { + c0: Fp::zero(), + c1: -Fp::one() + } + .sgn0() + ), + false + ); + assert_eq!( + bool::from( + Fp2 { + c0: P_M1_OVER2, + c1: p_p1_over2 + } + .sgn0() + ), + true + ); + assert_eq!( + bool::from( + Fp2 { + c0: p_p1_over2, + c1: P_M1_OVER2 + } + .sgn0() + ), + false + ); + + assert_eq!( + bool::from( + Fp2 { + c0: -Fp::one(), + c1: P_M1_OVER2, + } + .sgn0() + ), + false + ); + assert_eq!( + bool::from( + Fp2 { + c0: -Fp::one(), + c1: p_p1_over2, + } + .sgn0() + ), + false + ); + assert_eq!( + bool::from( + Fp2 { + c0: -Fp::one(), + c1: Fp::zero(), + } + .sgn0() + ), + false + ); + assert_eq!( + bool::from( + Fp2 { + c0: p_p1_over2, + c1: P_M1_OVER2, + } + .sgn0() + ), + false + ); + assert_eq!( + bool::from( + Fp2 { + c0: P_M1_OVER2, + c1: p_p1_over2, + } + .sgn0() + ), + true + ); +} diff --git a/constantine/bls12_381/src/hash_to_curve/map_scalar.rs b/constantine/bls12_381/src/hash_to_curve/map_scalar.rs new file mode 100644 index 000000000..ec4bb9e09 --- /dev/null +++ b/constantine/bls12_381/src/hash_to_curve/map_scalar.rs @@ -0,0 +1,39 @@ +//! Implementation of hash-to-field for Scalar values + +use super::HashToField; +use crate::generic_array::{typenum::U48, GenericArray}; +use crate::scalar::Scalar; + +impl HashToField for Scalar { + // ceil(log2(p)) = 255, m = 1, k = 128. + type InputLength = U48; + + fn from_okm(okm: &GenericArray) -> Scalar { + let mut bs = [0u8; 64]; + bs[16..].copy_from_slice(okm); + bs.reverse(); // into little endian + Scalar::from_bytes_wide(&bs) + } +} + +#[test] +fn test_hash_to_scalar() { + let tests: &[(&[u8], &str)] = &[ + ( + &[0u8; 48], + "0x0000000000000000000000000000000000000000000000000000000000000000", + ), + ( + b"aaaaaabbbbbbccccccddddddeeeeeeffffffgggggghhhhhh", + "0x2228450bf55d8fe62395161bd3677ff6fc28e45b89bc87e02a818eda11a8c5da", + ), + ( + b"111111222222333333444444555555666666777777888888", + "0x4aa543cbd2f0c8f37f8a375ce2e383eb343e7e3405f61e438b0a15fb8899d1ae", + ), + ]; + for (input, expected) in tests { + let output = format!("{:?}", Scalar::from_okm(GenericArray::from_slice(input))); + assert_eq!(&output, expected); + } +} diff --git a/constantine/bls12_381/src/hash_to_curve/mod.rs b/constantine/bls12_381/src/hash_to_curve/mod.rs new file mode 100644 index 000000000..4c15a498a --- /dev/null +++ b/constantine/bls12_381/src/hash_to_curve/mod.rs @@ -0,0 +1,113 @@ +//! This module implements hash_to_curve, hash_to_field and related +//! hashing primitives for use with BLS signatures. + +use core::ops::Add; + +use subtle::Choice; + +pub(crate) mod chain; + +mod expand_msg; +pub use self::expand_msg::{ + ExpandMessage, ExpandMessageState, ExpandMsgXmd, ExpandMsgXof, InitExpandMessage, +}; + +mod map_g1; +mod map_g2; +mod map_scalar; + +use crate::generic_array::{typenum::Unsigned, ArrayLength, GenericArray}; + +/// Enables a byte string to be hashed into one or more field elements for a given curve. +/// +/// Implements [section 5 of `draft-irtf-cfrg-hash-to-curve-12`][hash_to_field]. +/// +/// [hash_to_field]: https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-12#section-5 +pub trait HashToField: Sized { + /// The length of the data used to produce an individual field element. + /// + /// This must be set to `m * L = m * ceil((ceil(log2(p)) + k) / 8)`, where `p` is the + /// characteristic of `Self`, `m` is the extension degree of `Self`, and `k` is the + /// security parameter. + type InputLength: ArrayLength; + + /// Interprets the given output keying material as a big endian integer, and reduces + /// it into a field element. + fn from_okm(okm: &GenericArray) -> Self; + + /// Hashes a byte string of arbitrary length into one or more elements of `Self`, + /// using [`ExpandMessage`] variant `X`. + /// + /// Implements [section 5.3 of `draft-irtf-cfrg-hash-to-curve-12`][hash_to_field]. + /// + /// [hash_to_field]: https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-12#section-5.3 + fn hash_to_field(message: &[u8], dst: &[u8], output: &mut [Self]) { + let len_per_elm = Self::InputLength::to_usize(); + let len_in_bytes = output.len() * len_per_elm; + let mut expander = X::init_expand(message, dst, len_in_bytes); + + let mut buf = GenericArray::::default(); + output.iter_mut().for_each(|item| { + expander.read_into(&mut buf[..]); + *item = Self::from_okm(&buf); + }); + } +} + +/// Allow conversion from the output of hashed or encoded input into points on the curve +pub trait MapToCurve: Sized { + /// The field element type. + type Field: Copy + Default + HashToField; + + /// Maps an element of the finite field `Self::Field` to a point on the curve `Self`. + fn map_to_curve(elt: &Self::Field) -> Self; + + /// Clears the cofactor, sending a point on curve E to the target group (G1/G2). + fn clear_h(&self) -> Self; +} + +/// Implementation of random oracle maps to the curve. +pub trait HashToCurve: MapToCurve + for<'a> Add<&'a Self, Output = Self> { + /// Implements a uniform encoding from byte strings to elements of `Self`. + /// + /// This function is suitable for most applications requiring a random + /// oracle returning points in `Self`. + fn hash_to_curve(message: impl AsRef<[u8]>, dst: &[u8]) -> Self { + let mut u = [Self::Field::default(); 2]; + Self::Field::hash_to_field::(message.as_ref(), dst, &mut u); + let p1 = Self::map_to_curve(&u[0]); + let p2 = Self::map_to_curve(&u[1]); + (p1 + &p2).clear_h() + } + + /// Implements a **non-uniform** encoding from byte strings to elements of `Self`. + /// + /// The distribution of its output is not uniformly random in `Self`: the set of + /// possible outputs of this function is only a fraction of the points in `Self`, and + /// some elements of this set are more likely to be output than others. See + /// [section 10.1 of `draft-irtf-cfrg-hash-to-curve-12`][encode_to_curve-distribution] + /// for a more precise definition of `encode_to_curve`'s output distribution. + /// + /// [encode_to_curve-distribution]: https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-12#section-10.1 + fn encode_to_curve(message: impl AsRef<[u8]>, dst: &[u8]) -> Self { + let mut u = [Self::Field::default(); 1]; + Self::Field::hash_to_field::(message.as_ref(), dst, &mut u); + let p = Self::map_to_curve(&u[0]); + p.clear_h() + } +} + +impl HashToCurve for G +where + G: MapToCurve + for<'a> Add<&'a Self, Output = Self>, + X: ExpandMessage, +{ +} + +pub(crate) trait Sgn0 { + /// Returns either 0 or 1 indicating the "sign" of x, where sgn0(x) == 1 + /// just when x is "negative". (In other words, this function always considers 0 to be positive.) + /// + /// The equivalent for draft 6 would be `lexicographically_largest`. + fn sgn0(&self) -> Choice; +} diff --git a/constantine/bls12_381/src/lib.rs b/constantine/bls12_381/src/lib.rs new file mode 100644 index 000000000..6b8ce4c4f --- /dev/null +++ b/constantine/bls12_381/src/lib.rs @@ -0,0 +1,97 @@ +//! # `bls12_381` +//! +//! This crate provides an implementation of the BLS12-381 pairing-friendly elliptic +//! curve construction. +//! +//! * **This implementation has not been reviewed or audited. Use at your own risk.** +//! * This implementation targets Rust `1.36` or later. +//! * This implementation does not require the Rust standard library. +//! * All operations are constant time unless explicitly noted. + +#![no_std] +#![cfg_attr(docsrs, feature(doc_cfg))] +// Catch documentation errors caused by code changes. +#![deny(rustdoc::broken_intra_doc_links)] +#![deny(missing_debug_implementations)] +#![deny(unsafe_code)] +#![allow(clippy::too_many_arguments)] +#![allow(clippy::many_single_char_names)] +// This lint is described at +// https://rust-lang.github.io/rust-clippy/master/index.html#suspicious_arithmetic_impl +// In our library, some of the arithmetic involving extension fields will necessarily +// involve various binary operators, and so this lint is triggered unnecessarily. +#![allow(clippy::suspicious_arithmetic_impl)] + +#[cfg(feature = "alloc")] +extern crate alloc; + +#[cfg(test)] +#[macro_use] +extern crate std; + +#[cfg(test)] +#[cfg(feature = "groups")] +mod tests; + +#[macro_use] +mod util; + +/// Notes about how the BLS12-381 elliptic curve is designed, specified +/// and implemented by this library. +pub mod notes { + pub mod design; + pub mod serialization; +} + +pub mod scalar; + +pub use scalar::Scalar; + +#[cfg(feature = "groups")] +mod fp; +#[cfg(feature = "groups")] +pub mod fp2; +#[cfg(feature = "groups")] +pub mod g1; +#[cfg(feature = "groups")] +pub mod g2; + +#[cfg(feature = "groups")] +pub use g1::{G1Affine, G1Projective}; +#[cfg(feature = "groups")] +pub use g2::{G2Affine, G2Projective}; + +#[cfg(feature = "groups")] +mod fp12; +#[cfg(feature = "groups")] +mod fp6; + +// The BLS parameter x for BLS12-381 is -0xd201000000010000 +#[cfg(feature = "groups")] +const BLS_X: u64 = 0xd201_0000_0001_0000; +#[cfg(feature = "groups")] +const BLS_X_IS_NEGATIVE: bool = true; + +pub const MODULUS: Scalar = scalar::MODULUS; +pub const R2: Scalar = scalar::R2; + +#[cfg(feature = "pairings")] +mod pairings; + +#[cfg(feature = "pairings")] +pub use pairings::{pairing, Bls12, Gt, MillerLoopResult}; + +#[cfg(all(feature = "pairings", feature = "alloc"))] +pub use pairings::{multi_miller_loop, G2Prepared}; + +pub use fp::Fp; +pub use fp2::Fp2; +pub use fp6::Fp6; +pub use fp12::Fp12; + +/// Use the generic_array re-exported by digest to avoid a version mismatch +#[cfg(feature = "experimental")] +pub(crate) use digest::generic_array; + +#[cfg(feature = "experimental")] +pub mod hash_to_curve; diff --git a/constantine/bls12_381/src/notes/design.rs b/constantine/bls12_381/src/notes/design.rs new file mode 100644 index 000000000..685a7e75f --- /dev/null +++ b/constantine/bls12_381/src/notes/design.rs @@ -0,0 +1,108 @@ +//! # Design of BLS12-381 +//! ## Fixed Generators +//! +//! Although any generator produced by hashing to $\mathbb{G}_1$ or $\mathbb{G}_2$ is +//! safe to use in a cryptographic protocol, we specify some simple, fixed generators. +//! +//! In order to derive these generators, we select the lexicographically smallest +//! valid $x$-coordinate and the lexicographically smallest corresponding $y$-coordinate, +//! and then scale the resulting point by the cofactor, such that the result is not the +//! identity. This results in the following fixed generators: +//! +//! 1. $\mathbb{G}_1$ +//! * $x = 3685416753713387016781088315183077757961620795782546409894578378688607592378376318836054947676345821548104185464507$ +//! * $y = 1339506544944476473020471379941921221584933875938349620426543736416511423956333506472724655353366534992391756441569$ +//! 2. $\mathbb{G}_2$ +//! * $x = 352701069587466618187139116011060144890029952792775240219908644239793785735715026873347600343865175952761926303160 + 3059144344244213709971259814753781636986470325476647558659373206291635324768958432433509563104347017837885763365758 u$ +//! * $y = 1985150602287291935568054521177171638300868978215655730859378665066344726373823718423869104263333984641494340347905 + 927553665492332455747201965776037880757740193453592970025027978793976877002675564980949289727957565575433344219582 u$ +//! +//! This can be derived using the following sage script: +//! +//! ```text +//! param = -0xd201000000010000 +//! def r(x): +//! return (x**4) - (x**2) + 1 +//! def q(x): +//! return (((x - 1) ** 2) * ((x**4) - (x**2) + 1) // 3) + x +//! def g1_h(x): +//! return ((x-1)**2) // 3 +//! def g2_h(x): +//! return ((x**8) - (4 * (x**7)) + (5 * (x**6)) - (4 * (x**4)) + (6 * (x**3)) - (4 * (x**2)) - (4*x) + 13) // 9 +//! q = q(param) +//! r = r(param) +//! Fq = GF(q) +//! ec = EllipticCurve(Fq, [0, 4]) +//! def psqrt(v): +//! assert(not v.is_zero()) +//! a = sqrt(v) +//! b = -a +//! if a < b: +//! return a +//! else: +//! return b +//! for x in range(0,100): +//! rhs = Fq(x)^3 + 4 +//! if rhs.is_square(): +//! y = psqrt(rhs) +//! p = ec(x, y) * g1_h(param) +//! if (not p.is_zero()) and (p * r).is_zero(): +//! print("g1 generator: {}".format(p)) +//! break +//! Fq2. = GF(q^2, modulus=[1, 0, 1]) +//! ec2 = EllipticCurve(Fq2, [0, (4 * (1 + i))]) +//! assert(ec2.order() == (r * g2_h(param))) +//! for x in range(0,100): +//! rhs = (Fq2(x))^3 + (4 * (1 + i)) +//! if rhs.is_square(): +//! y = psqrt(rhs) +//! p = ec2(Fq2(x), y) * g2_h(param) +//! if not p.is_zero() and (p * r).is_zero(): +//! print("g2 generator: {}".format(p)) +//! break +//! ``` +//! +//! ## Nontrivial third root of unity +//! +//! To use the fast subgroup check algorithm for $\mathbb{G_1}$ from https://eprint.iacr.org/2019/814.pdf and +//! https://eprint.iacr.org/2021/1130, it is necessary to find a nontrivial cube root of +//! unity ฮฒ in Fp to define the endomorphism: +//! $$(x, y) \rightarrow (\beta x, y)$$ +//! which is equivalent to +//! $$P \rightarrow \lambda P$$ +//! where $\lambda$, a nontrivial cube root of unity in Fr, satisfies $\lambda^2 + \lambda +1 = 0 \pmod{r}. +//! +//! $$\beta = 793479390729215512621379701633421447060886740281060493010456487427281649075476305620758731620350$$ +//! can be derived using the following sage commands after running the above sage script: +//! +//! ```text +//! # Prints the given field element in Montgomery form. +//! def print_fq(a): +//! R = 1 << 384 +//! tmp = ZZ(Fq(a*R)) +//! while tmp > 0: +//! print("0x{:_x}, ".format(tmp % (1 << 64))) +//! tmp >>= 64 +//! ฮฒ = (Fq.multiplicative_generator() ** ((q-1)/3)) +//! print_fq(ฮฒ) +//! ``` +//! +//! ## Psi +//! +//! To use the fast subgroup check algorithm for $\mathbb{G_2}$ from https://eprint.iacr.org/2019/814.pdf and +//! https://eprint.iacr.org/2021/1130, it is necessary to find the endomorphism: +//! +//! $$(x, y, z) \rightarrow (x^q \psi_x, y^q \psi_y, z^q)$$ +//! +//! where: +//! +//! 1. $\psi_x = 1 / ((i+1) ^ ((q-1)/3)) \in \mathbb{F}_{q^2}$, and +//! 2. $\psi_y = 1 / ((i+1) ^ ((q-1)/2)) \in \mathbb{F}_{q^2}$ +//! +//! can be derived using the following sage commands after running the above script and commands: +//! ```text +//! psi_x = (1/((i+1)**((q-1)/3))) +//! psi_y = (1/((i+1)**((q-1)/2))) +//! print_fq(psi_x.polynomial().coefficients()[0]) +//! print_fq(psi_y.polynomial().coefficients()[0]) +//! print_fq(psi_y.polynomial().coefficients()[1]) +//! ``` diff --git a/constantine/bls12_381/src/notes/serialization.rs b/constantine/bls12_381/src/notes/serialization.rs new file mode 100644 index 000000000..ded752e6e --- /dev/null +++ b/constantine/bls12_381/src/notes/serialization.rs @@ -0,0 +1,29 @@ +//! # BLS12-381 serialization +//! +//! * $\mathbb{F}\_p$ elements are encoded in big-endian form. They occupy 48 +//! bytes in this form. +//! * $\mathbb{F}\_{p^2}$ elements are encoded in big-endian form, meaning that +//! the $\mathbb{F}\_{p^2}$ element $c\_0 + c\_1 \cdot u$ is represented by the +//! $\mathbb{F}\_p$ element $c\_1$ followed by the $\mathbb{F}\_p$ element $c\_0$. +//! This means $\mathbb{F}_{p^2}$ elements occupy 96 bytes in this form. +//! * The group $\mathbb{G}\_1$ uses $\mathbb{F}\_p$ elements for coordinates. The +//! group $\mathbb{G}\_2$ uses $\mathbb{F}_{p^2}$ elements for coordinates. +//! * $\mathbb{G}\_1$ and $\mathbb{G}\_2$ elements can be encoded in uncompressed +//! form (the x-coordinate followed by the y-coordinate) or in compressed form +//! (just the x-coordinate). $\mathbb{G}\_1$ elements occupy 96 bytes in +//! uncompressed form, and 48 bytes in compressed form. $\mathbb{G}\_2$ +//! elements occupy 192 bytes in uncompressed form, and 96 bytes in compressed +//! form. +//! +//! The most-significant three bits of a $\mathbb{G}\_1$ or $\mathbb{G}\_2$ +//! encoding should be masked away before the coordinate(s) are interpreted. +//! These bits are used to unambiguously represent the underlying element: +//! * The most significant bit, when set, indicates that the point is in +//! compressed form. Otherwise, the point is in uncompressed form. +//! * The second-most significant bit indicates that the point is at infinity. +//! If this bit is set, the remaining bits of the group element's encoding +//! should be set to zero. +//! * The third-most significant bit is set if (and only if) this point is in +//! compressed form _and_ it is not the point at infinity _and_ its +//! y-coordinate is the lexicographically largest of the two associated with +//! the encoded x-coordinate. diff --git a/constantine/bls12_381/src/pairings.rs b/constantine/bls12_381/src/pairings.rs new file mode 100644 index 000000000..2a9d923a2 --- /dev/null +++ b/constantine/bls12_381/src/pairings.rs @@ -0,0 +1,970 @@ +use crate::fp::Fp; +use crate::fp12::Fp12; +use crate::fp2::Fp2; +use crate::fp6::Fp6; +use crate::{G1Affine, G1Projective, G2Affine, G2Projective, Scalar, BLS_X, BLS_X_IS_NEGATIVE}; + +use core::borrow::Borrow; +use core::fmt; +use core::iter::Sum; +use core::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}; +use group::Group; +use pairing::{Engine, PairingCurveAffine}; +use rand_core::RngCore; +use subtle::{Choice, ConditionallySelectable, ConstantTimeEq}; + +#[cfg(feature = "alloc")] +use alloc::vec::Vec; +#[cfg(feature = "alloc")] +use pairing::MultiMillerLoop; + +/// Represents results of a Miller loop, one of the most expensive portions +/// of the pairing function. `MillerLoopResult`s cannot be compared with each +/// other until `.final_exponentiation()` is called, which is also expensive. +#[cfg_attr(docsrs, doc(cfg(feature = "pairings")))] +#[derive(Copy, Clone, Debug)] +pub struct MillerLoopResult(pub(crate) Fp12); + +impl Default for MillerLoopResult { + fn default() -> Self { + MillerLoopResult(Fp12::one()) + } +} + +#[cfg(feature = "zeroize")] +impl zeroize::DefaultIsZeroes for MillerLoopResult {} + +impl ConditionallySelectable for MillerLoopResult { + fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self { + MillerLoopResult(Fp12::conditional_select(&a.0, &b.0, choice)) + } +} + +impl MillerLoopResult { + /// This performs a "final exponentiation" routine to convert the result + /// of a Miller loop into an element of `Gt` with help of efficient squaring + /// operation in the so-called `cyclotomic subgroup` of `Fq6` so that + /// it can be compared with other elements of `Gt`. + pub fn final_exponentiation(&self) -> Gt { + #[must_use] + fn fp4_square(a: Fp2, b: Fp2) -> (Fp2, Fp2) { + let t0 = a.square(); + let t1 = b.square(); + let mut t2 = t1.mul_by_nonresidue(); + let c0 = t2 + t0; + t2 = a + b; + t2 = t2.square(); + t2 -= t0; + let c1 = t2 - t1; + + (c0, c1) + } + // Adaptation of Algorithm 5.5.4, Guide to Pairing-Based Cryptography + // Faster Squaring in the Cyclotomic Subgroup of Sixth Degree Extensions + // https://eprint.iacr.org/2009/565.pdf + #[must_use] + fn cyclotomic_square(f: Fp12) -> Fp12 { + let mut z0 = f.c0.c0; + let mut z4 = f.c0.c1; + let mut z3 = f.c0.c2; + let mut z2 = f.c1.c0; + let mut z1 = f.c1.c1; + let mut z5 = f.c1.c2; + + let (t0, t1) = fp4_square(z0, z1); + + // For A + z0 = t0 - z0; + z0 = z0 + z0 + t0; + + z1 = t1 + z1; + z1 = z1 + z1 + t1; + + let (mut t0, t1) = fp4_square(z2, z3); + let (t2, t3) = fp4_square(z4, z5); + + // For C + z4 = t0 - z4; + z4 = z4 + z4 + t0; + + z5 = t1 + z5; + z5 = z5 + z5 + t1; + + // For B + t0 = t3.mul_by_nonresidue(); + z2 = t0 + z2; + z2 = z2 + z2 + t0; + + z3 = t2 - z3; + z3 = z3 + z3 + t2; + + Fp12 { + c0: Fp6 { + c0: z0, + c1: z4, + c2: z3, + }, + c1: Fp6 { + c0: z2, + c1: z1, + c2: z5, + }, + } + } + #[must_use] + fn cycolotomic_exp(f: Fp12) -> Fp12 { + let x = BLS_X; + let mut tmp = Fp12::one(); + let mut found_one = false; + for i in (0..64).rev().map(|b| ((x >> b) & 1) == 1) { + if found_one { + tmp = cyclotomic_square(tmp) + } else { + found_one = i; + } + + if i { + tmp *= f; + } + } + + tmp.conjugate() + } + + let mut f = self.0; + let mut t0 = f + .frobenius_map() + .frobenius_map() + .frobenius_map() + .frobenius_map() + .frobenius_map() + .frobenius_map(); + Gt(f.invert() + .map(|mut t1| { + let mut t2 = t0 * t1; + t1 = t2; + t2 = t2.frobenius_map().frobenius_map(); + t2 *= t1; + t1 = cyclotomic_square(t2).conjugate(); + let mut t3 = cycolotomic_exp(t2); + let mut t4 = cyclotomic_square(t3); + let mut t5 = t1 * t3; + t1 = cycolotomic_exp(t5); + t0 = cycolotomic_exp(t1); + let mut t6 = cycolotomic_exp(t0); + t6 *= t4; + t4 = cycolotomic_exp(t6); + t5 = t5.conjugate(); + t4 *= t5 * t2; + t5 = t2.conjugate(); + t1 *= t2; + t1 = t1.frobenius_map().frobenius_map().frobenius_map(); + t6 *= t5; + t6 = t6.frobenius_map(); + t3 *= t0; + t3 = t3.frobenius_map().frobenius_map(); + t3 *= t1; + t3 *= t6; + f = t3 * t4; + + f + }) + // We unwrap() because `MillerLoopResult` can only be constructed + // by a function within this crate, and we uphold the invariant + // that the enclosed value is nonzero. + .unwrap()) + } +} + +impl<'a, 'b> Add<&'b MillerLoopResult> for &'a MillerLoopResult { + type Output = MillerLoopResult; + + #[inline] + fn add(self, rhs: &'b MillerLoopResult) -> MillerLoopResult { + MillerLoopResult(self.0 * rhs.0) + } +} + +impl_add_binop_specify_output!(MillerLoopResult, MillerLoopResult, MillerLoopResult); + +impl AddAssign for MillerLoopResult { + #[inline] + fn add_assign(&mut self, rhs: MillerLoopResult) { + *self = *self + rhs; + } +} + +impl<'b> AddAssign<&'b MillerLoopResult> for MillerLoopResult { + #[inline] + fn add_assign(&mut self, rhs: &'b MillerLoopResult) { + *self = *self + rhs; + } +} + +/// This is an element of $\mathbb{G}_T$, the target group of the pairing function. As with +/// $\mathbb{G}_1$ and $\mathbb{G}_2$ this group has order $q$. +/// +/// Typically, $\mathbb{G}_T$ is written multiplicatively but we will write it additively to +/// keep code and abstractions consistent. +#[cfg_attr(docsrs, doc(cfg(feature = "pairings")))] +#[derive(Copy, Clone, Debug)] +pub struct Gt(pub Fp12); + +impl Default for Gt { + fn default() -> Self { + Self::identity() + } +} + +#[cfg(feature = "zeroize")] +impl zeroize::DefaultIsZeroes for Gt {} + +impl fmt::Display for Gt { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +impl ConstantTimeEq for Gt { + fn ct_eq(&self, other: &Self) -> Choice { + self.0.ct_eq(&other.0) + } +} + +impl ConditionallySelectable for Gt { + fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self { + Gt(Fp12::conditional_select(&a.0, &b.0, choice)) + } +} + +impl Eq for Gt {} +impl PartialEq for Gt { + #[inline] + fn eq(&self, other: &Self) -> bool { + bool::from(self.ct_eq(other)) + } +} + +impl Gt { + /// Returns the group identity, which is $1$. + pub fn identity() -> Gt { + Gt(Fp12::one()) + } + + /// Doubles this group element. + pub fn double(&self) -> Gt { + Gt(self.0.square()) + } +} + +impl<'a> Neg for &'a Gt { + type Output = Gt; + + #[inline] + fn neg(self) -> Gt { + // The element is unitary, so we just conjugate. + Gt(self.0.conjugate()) + } +} + +impl Neg for Gt { + type Output = Gt; + + #[inline] + fn neg(self) -> Gt { + -&self + } +} + +impl<'a, 'b> Add<&'b Gt> for &'a Gt { + type Output = Gt; + + #[inline] + fn add(self, rhs: &'b Gt) -> Gt { + Gt(self.0 * rhs.0) + } +} + +impl<'a, 'b> Sub<&'b Gt> for &'a Gt { + type Output = Gt; + + #[inline] + fn sub(self, rhs: &'b Gt) -> Gt { + self + (-rhs) + } +} + +impl<'a, 'b> Mul<&'b Scalar> for &'a Gt { + type Output = Gt; + + fn mul(self, other: &'b Scalar) -> Self::Output { + let mut acc = Gt::identity(); + + // This is a simple double-and-add implementation of group element + // multiplication, moving from most significant to least + // significant bit of the scalar. + // + // We skip the leading bit because it's always unset for Fq + // elements. + for bit in other + .to_bytes() + .iter() + .rev() + .flat_map(|byte| (0..8).rev().map(move |i| Choice::from((byte >> i) & 1u8))) + .skip(1) + { + acc = acc.double(); + acc = Gt::conditional_select(&acc, &(acc + self), bit); + } + + acc + } +} + +impl_binops_additive!(Gt, Gt); +impl_binops_multiplicative!(Gt, Scalar); + +impl Sum for Gt +where + T: Borrow, +{ + fn sum(iter: I) -> Self + where + I: Iterator, + { + iter.fold(Self::identity(), |acc, item| acc + item.borrow()) + } +} + +impl Group for Gt { + type Scalar = Scalar; + + fn random(mut rng: impl RngCore) -> Self { + loop { + let inner = Fp12::random(&mut rng); + + // Not all elements of Fp12 are elements of the prime-order multiplicative + // subgroup. We run the random element through final_exponentiation to obtain + // a valid element, which requires that it is non-zero. + if !bool::from(inner.is_zero()) { + return MillerLoopResult(inner).final_exponentiation(); + } + } + } + + fn identity() -> Self { + Self::identity() + } + + fn generator() -> Self { + // pairing(&G1Affine::generator(), &G2Affine::generator()) + Gt(Fp12 { + c0: Fp6 { + c0: Fp2 { + c0: Fp::from_raw_unchecked([ + 0x1972_e433_a01f_85c5, + 0x97d3_2b76_fd77_2538, + 0xc8ce_546f_c96b_cdf9, + 0xcef6_3e73_66d4_0614, + 0xa611_3427_8184_3780, + 0x13f3_448a_3fc6_d825, + ]), + c1: Fp::from_raw_unchecked([ + 0xd263_31b0_2e9d_6995, + 0x9d68_a482_f779_7e7d, + 0x9c9b_2924_8d39_ea92, + 0xf480_1ca2_e131_07aa, + 0xa16c_0732_bdbc_b066, + 0x083c_a4af_ba36_0478, + ]), + }, + c1: Fp2 { + c0: Fp::from_raw_unchecked([ + 0x59e2_61db_0916_b641, + 0x2716_b6f4_b23e_960d, + 0xc8e5_5b10_a0bd_9c45, + 0x0bdb_0bd9_9c4d_eda8, + 0x8cf8_9ebf_57fd_aac5, + 0x12d6_b792_9e77_7a5e, + ]), + c1: Fp::from_raw_unchecked([ + 0x5fc8_5188_b0e1_5f35, + 0x34a0_6e3a_8f09_6365, + 0xdb31_26a6_e02a_d62c, + 0xfc6f_5aa9_7d9a_990b, + 0xa12f_55f5_eb89_c210, + 0x1723_703a_926f_8889, + ]), + }, + c2: Fp2 { + c0: Fp::from_raw_unchecked([ + 0x9358_8f29_7182_8778, + 0x43f6_5b86_11ab_7585, + 0x3183_aaf5_ec27_9fdf, + 0xfa73_d7e1_8ac9_9df6, + 0x64e1_76a6_a64c_99b0, + 0x179f_a78c_5838_8f1f, + ]), + c1: Fp::from_raw_unchecked([ + 0x672a_0a11_ca2a_ef12, + 0x0d11_b9b5_2aa3_f16b, + 0xa444_12d0_699d_056e, + 0xc01d_0177_221a_5ba5, + 0x66e0_cede_6c73_5529, + 0x05f5_a71e_9fdd_c339, + ]), + }, + }, + c1: Fp6 { + c0: Fp2 { + c0: Fp::from_raw_unchecked([ + 0xd30a_88a1_b062_c679, + 0x5ac5_6a5d_35fc_8304, + 0xd0c8_34a6_a81f_290d, + 0xcd54_30c2_da37_07c7, + 0xf0c2_7ff7_8050_0af0, + 0x0924_5da6_e2d7_2eae, + ]), + c1: Fp::from_raw_unchecked([ + 0x9f2e_0676_791b_5156, + 0xe2d1_c823_4918_fe13, + 0x4c9e_459f_3c56_1bf4, + 0xa3e8_5e53_b9d3_e3c1, + 0x820a_121e_21a7_0020, + 0x15af_6183_41c5_9acc, + ]), + }, + c1: Fp2 { + c0: Fp::from_raw_unchecked([ + 0x7c95_658c_2499_3ab1, + 0x73eb_3872_1ca8_86b9, + 0x5256_d749_4774_34bc, + 0x8ba4_1902_ea50_4a8b, + 0x04a3_d3f8_0c86_ce6d, + 0x18a6_4a87_fb68_6eaa, + ]), + c1: Fp::from_raw_unchecked([ + 0xbb83_e71b_b920_cf26, + 0x2a52_77ac_92a7_3945, + 0xfc0e_e59f_94f0_46a0, + 0x7158_cdf3_7860_58f7, + 0x7cc1_061b_82f9_45f6, + 0x03f8_47aa_9fdb_e567, + ]), + }, + c2: Fp2 { + c0: Fp::from_raw_unchecked([ + 0x8078_dba5_6134_e657, + 0x1cd7_ec9a_4399_8a6e, + 0xb1aa_599a_1a99_3766, + 0xc9a0_f62f_0842_ee44, + 0x8e15_9be3_b605_dffa, + 0x0c86_ba0d_4af1_3fc2, + ]), + c1: Fp::from_raw_unchecked([ + 0xe80f_f2a0_6a52_ffb1, + 0x7694_ca48_721a_906c, + 0x7583_183e_03b0_8514, + 0xf567_afdd_40ce_e4e2, + 0x9a6d_96d2_e526_a5fc, + 0x197e_9f49_861f_2242, + ]), + }, + }, + }) + } + + fn is_identity(&self) -> Choice { + self.ct_eq(&Self::identity()) + } + + #[must_use] + fn double(&self) -> Self { + self.double() + } +} + +#[cfg(feature = "alloc")] +#[cfg_attr(docsrs, doc(cfg(all(feature = "pairings", feature = "alloc"))))] +#[derive(Clone, Debug)] +/// This structure contains cached computations pertaining to a $\mathbb{G}_2$ +/// element as part of the pairing function (specifically, the Miller loop) and +/// so should be computed whenever a $\mathbb{G}_2$ element is being used in +/// multiple pairings or is otherwise known in advance. This should be used in +/// conjunction with the [`multi_miller_loop`](crate::multi_miller_loop) +/// function provided by this crate. +/// +/// Requires the `alloc` and `pairing` crate features to be enabled. +pub struct G2Prepared { + infinity: Choice, + coeffs: Vec<(Fp2, Fp2, Fp2)>, +} + +#[cfg(feature = "alloc")] +impl From for G2Prepared { + fn from(q: G2Affine) -> G2Prepared { + struct Adder { + cur: G2Projective, + base: G2Affine, + coeffs: Vec<(Fp2, Fp2, Fp2)>, + } + + impl MillerLoopDriver for Adder { + type Output = (); + + fn doubling_step(&mut self, _: Self::Output) -> Self::Output { + let coeffs = doubling_step(&mut self.cur); + self.coeffs.push(coeffs); + } + fn addition_step(&mut self, _: Self::Output) -> Self::Output { + let coeffs = addition_step(&mut self.cur, &self.base); + self.coeffs.push(coeffs); + } + fn square_output(_: Self::Output) -> Self::Output {} + fn conjugate(_: Self::Output) -> Self::Output {} + fn one() -> Self::Output {} + } + + let is_identity = q.is_identity(); + let q = G2Affine::conditional_select(&q, &G2Affine::generator(), is_identity); + + let mut adder = Adder { + cur: G2Projective::from(q), + base: q, + coeffs: Vec::with_capacity(68), + }; + + miller_loop(&mut adder); + + assert_eq!(adder.coeffs.len(), 68); + + G2Prepared { + infinity: is_identity, + coeffs: adder.coeffs, + } + } +} + +#[cfg(feature = "alloc")] +#[cfg_attr(docsrs, doc(cfg(all(feature = "pairings", feature = "alloc"))))] +/// Computes $$\sum_{i=1}^n \textbf{ML}(a_i, b_i)$$ given a series of terms +/// $$(a_1, b_1), (a_2, b_2), ..., (a_n, b_n).$$ +/// +/// Requires the `alloc` and `pairing` crate features to be enabled. +pub fn multi_miller_loop(terms: &[(&G1Affine, &G2Prepared)]) -> MillerLoopResult { + struct Adder<'a, 'b, 'c> { + terms: &'c [(&'a G1Affine, &'b G2Prepared)], + index: usize, + } + + impl<'a, 'b, 'c> MillerLoopDriver for Adder<'a, 'b, 'c> { + type Output = Fp12; + + fn doubling_step(&mut self, mut f: Self::Output) -> Self::Output { + let index = self.index; + for term in self.terms { + let either_identity = term.0.is_identity() | term.1.infinity; + + let new_f = ell(f, &term.1.coeffs[index], term.0); + f = Fp12::conditional_select(&new_f, &f, either_identity); + } + self.index += 1; + + f + } + fn addition_step(&mut self, mut f: Self::Output) -> Self::Output { + let index = self.index; + for term in self.terms { + let either_identity = term.0.is_identity() | term.1.infinity; + + let new_f = ell(f, &term.1.coeffs[index], term.0); + f = Fp12::conditional_select(&new_f, &f, either_identity); + } + self.index += 1; + + f + } + fn square_output(f: Self::Output) -> Self::Output { + f.square() + } + fn conjugate(f: Self::Output) -> Self::Output { + f.conjugate() + } + fn one() -> Self::Output { + Fp12::one() + } + } + + let mut adder = Adder { terms, index: 0 }; + + let tmp = miller_loop(&mut adder); + + MillerLoopResult(tmp) +} + +/// Invoke the pairing function without the use of precomputation and other optimizations. +#[cfg_attr(docsrs, doc(cfg(feature = "pairings")))] +pub fn pairing(p: &G1Affine, q: &G2Affine) -> Gt { + struct Adder { + cur: G2Projective, + base: G2Affine, + p: G1Affine, + } + + impl MillerLoopDriver for Adder { + type Output = Fp12; + + fn doubling_step(&mut self, f: Self::Output) -> Self::Output { + let coeffs = doubling_step(&mut self.cur); + ell(f, &coeffs, &self.p) + } + fn addition_step(&mut self, f: Self::Output) -> Self::Output { + let coeffs = addition_step(&mut self.cur, &self.base); + ell(f, &coeffs, &self.p) + } + fn square_output(f: Self::Output) -> Self::Output { + f.square() + } + fn conjugate(f: Self::Output) -> Self::Output { + f.conjugate() + } + fn one() -> Self::Output { + Fp12::one() + } + } + + let either_identity = p.is_identity() | q.is_identity(); + let p = G1Affine::conditional_select(p, &G1Affine::generator(), either_identity); + let q = G2Affine::conditional_select(q, &G2Affine::generator(), either_identity); + + let mut adder = Adder { + cur: G2Projective::from(q), + base: q, + p, + }; + + let tmp = miller_loop(&mut adder); + let tmp = MillerLoopResult(Fp12::conditional_select( + &tmp, + &Fp12::one(), + either_identity, + )); + tmp.final_exponentiation() +} + +trait MillerLoopDriver { + type Output; + + fn doubling_step(&mut self, f: Self::Output) -> Self::Output; + fn addition_step(&mut self, f: Self::Output) -> Self::Output; + fn square_output(f: Self::Output) -> Self::Output; + fn conjugate(f: Self::Output) -> Self::Output; + fn one() -> Self::Output; +} + +/// This is a "generic" implementation of the Miller loop to avoid duplicating code +/// structure elsewhere; instead, we'll write concrete instantiations of +/// `MillerLoopDriver` for whatever purposes we need (such as caching modes). +fn miller_loop(driver: &mut D) -> D::Output { + let mut f = D::one(); + + let mut found_one = false; + for i in (0..64).rev().map(|b| (((BLS_X >> 1) >> b) & 1) == 1) { + if !found_one { + found_one = i; + continue; + } + + f = driver.doubling_step(f); + + if i { + f = driver.addition_step(f); + } + + f = D::square_output(f); + } + + f = driver.doubling_step(f); + + if BLS_X_IS_NEGATIVE { + f = D::conjugate(f); + } + + f +} + +fn ell(f: Fp12, coeffs: &(Fp2, Fp2, Fp2), p: &G1Affine) -> Fp12 { + let mut c0 = coeffs.0; + let mut c1 = coeffs.1; + + c0.c0 *= p.y; + c0.c1 *= p.y; + + c1.c0 *= p.x; + c1.c1 *= p.x; + + f.mul_by_014(&coeffs.2, &c1, &c0) +} + +fn doubling_step(r: &mut G2Projective) -> (Fp2, Fp2, Fp2) { + // Adaptation of Algorithm 26, https://eprint.iacr.org/2010/354.pdf + let tmp0 = r.x.square(); + let tmp1 = r.y.square(); + let tmp2 = tmp1.square(); + let tmp3 = (tmp1 + r.x).square() - tmp0 - tmp2; + let tmp3 = tmp3 + tmp3; + let tmp4 = tmp0 + tmp0 + tmp0; + let tmp6 = r.x + tmp4; + let tmp5 = tmp4.square(); + let zsquared = r.z.square(); + r.x = tmp5 - tmp3 - tmp3; + r.z = (r.z + r.y).square() - tmp1 - zsquared; + r.y = (tmp3 - r.x) * tmp4; + let tmp2 = tmp2 + tmp2; + let tmp2 = tmp2 + tmp2; + let tmp2 = tmp2 + tmp2; + r.y -= tmp2; + let tmp3 = tmp4 * zsquared; + let tmp3 = tmp3 + tmp3; + let tmp3 = -tmp3; + let tmp6 = tmp6.square() - tmp0 - tmp5; + let tmp1 = tmp1 + tmp1; + let tmp1 = tmp1 + tmp1; + let tmp6 = tmp6 - tmp1; + let tmp0 = r.z * zsquared; + let tmp0 = tmp0 + tmp0; + + (tmp0, tmp3, tmp6) +} + +fn addition_step(r: &mut G2Projective, q: &G2Affine) -> (Fp2, Fp2, Fp2) { + // Adaptation of Algorithm 27, https://eprint.iacr.org/2010/354.pdf + let zsquared = r.z.square(); + let ysquared = q.y.square(); + let t0 = zsquared * q.x; + let t1 = ((q.y + r.z).square() - ysquared - zsquared) * zsquared; + let t2 = t0 - r.x; + let t3 = t2.square(); + let t4 = t3 + t3; + let t4 = t4 + t4; + let t5 = t4 * t2; + let t6 = t1 - r.y - r.y; + let t9 = t6 * q.x; + let t7 = t4 * r.x; + r.x = t6.square() - t5 - t7 - t7; + r.z = (r.z + t2).square() - zsquared - t3; + let t10 = q.y + r.z; + let t8 = (t7 - r.x) * t6; + let t0 = r.y * t5; + let t0 = t0 + t0; + r.y = t8 - t0; + let t10 = t10.square() - ysquared; + let ztsquared = r.z.square(); + let t10 = t10 - ztsquared; + let t9 = t9 + t9 - t10; + let t10 = r.z + r.z; + let t6 = -t6; + let t1 = t6 + t6; + + (t10, t1, t9) +} + +impl PairingCurveAffine for G1Affine { + type Pair = G2Affine; + type PairingResult = Gt; + + fn pairing_with(&self, other: &Self::Pair) -> Self::PairingResult { + pairing(self, other) + } +} + +impl PairingCurveAffine for G2Affine { + type Pair = G1Affine; + type PairingResult = Gt; + + fn pairing_with(&self, other: &Self::Pair) -> Self::PairingResult { + pairing(other, self) + } +} + +/// A [`pairing::Engine`] for BLS12-381 pairing operations. +#[cfg_attr(docsrs, doc(cfg(feature = "pairings")))] +#[derive(Clone, Debug)] +pub struct Bls12; + +impl Engine for Bls12 { + type Fr = Scalar; + type G1 = G1Projective; + type G1Affine = G1Affine; + type G2 = G2Projective; + type G2Affine = G2Affine; + type Gt = Gt; + + fn pairing(p: &Self::G1Affine, q: &Self::G2Affine) -> Self::Gt { + pairing(p, q) + } +} + +impl pairing::MillerLoopResult for MillerLoopResult { + type Gt = Gt; + + fn final_exponentiation(&self) -> Self::Gt { + self.final_exponentiation() + } +} + +#[cfg(feature = "alloc")] +impl MultiMillerLoop for Bls12 { + type G2Prepared = G2Prepared; + type Result = MillerLoopResult; + + fn multi_miller_loop(terms: &[(&Self::G1Affine, &Self::G2Prepared)]) -> Self::Result { + multi_miller_loop(terms) + } +} + +#[test] +fn test_gt_generator() { + assert_eq!( + Gt::generator(), + pairing(&G1Affine::generator(), &G2Affine::generator()) + ); +} + +#[test] +fn test_bilinearity() { + use crate::Scalar; + + let a = Scalar::from_raw([1, 2, 3, 4]).invert().unwrap().square(); + let b = Scalar::from_raw([5, 6, 7, 8]).invert().unwrap().square(); + let c = a * b; + + let g = G1Affine::from(G1Affine::generator() * a); + let h = G2Affine::from(G2Affine::generator() * b); + let p = pairing(&g, &h); + + assert!(p != Gt::identity()); + + let expected = G1Affine::from(G1Affine::generator() * c); + + assert_eq!(p, pairing(&expected, &G2Affine::generator())); + assert_eq!( + p, + pairing(&G1Affine::generator(), &G2Affine::generator()) * c + ); +} + +#[test] +fn test_unitary() { + let g = G1Affine::generator(); + let h = G2Affine::generator(); + let p = -pairing(&g, &h); + let q = pairing(&g, &-h); + let r = pairing(&-g, &h); + + assert_eq!(p, q); + assert_eq!(q, r); +} + +#[cfg(feature = "alloc")] +#[test] +fn test_multi_miller_loop() { + let a1 = G1Affine::generator(); + let b1 = G2Affine::generator(); + + let a2 = G1Affine::from( + G1Affine::generator() * Scalar::from_raw([1, 2, 3, 4]).invert().unwrap().square(), + ); + let b2 = G2Affine::from( + G2Affine::generator() * Scalar::from_raw([4, 2, 2, 4]).invert().unwrap().square(), + ); + + let a3 = G1Affine::identity(); + let b3 = G2Affine::from( + G2Affine::generator() * Scalar::from_raw([9, 2, 2, 4]).invert().unwrap().square(), + ); + + let a4 = G1Affine::from( + G1Affine::generator() * Scalar::from_raw([5, 5, 5, 5]).invert().unwrap().square(), + ); + let b4 = G2Affine::identity(); + + let a5 = G1Affine::from( + G1Affine::generator() * Scalar::from_raw([323, 32, 3, 1]).invert().unwrap().square(), + ); + let b5 = G2Affine::from( + G2Affine::generator() * Scalar::from_raw([4, 2, 2, 9099]).invert().unwrap().square(), + ); + + let b1_prepared = G2Prepared::from(b1); + let b2_prepared = G2Prepared::from(b2); + let b3_prepared = G2Prepared::from(b3); + let b4_prepared = G2Prepared::from(b4); + let b5_prepared = G2Prepared::from(b5); + + let expected = pairing(&a1, &b1) + + pairing(&a2, &b2) + + pairing(&a3, &b3) + + pairing(&a4, &b4) + + pairing(&a5, &b5); + + let test = multi_miller_loop(&[ + (&a1, &b1_prepared), + (&a2, &b2_prepared), + (&a3, &b3_prepared), + (&a4, &b4_prepared), + (&a5, &b5_prepared), + ]) + .final_exponentiation(); + + assert_eq!(expected, test); +} + +#[test] +fn test_miller_loop_result_default() { + assert_eq!( + MillerLoopResult::default().final_exponentiation(), + Gt::identity(), + ); +} + +#[cfg(feature = "zeroize")] +#[test] +fn test_miller_loop_result_zeroize() { + use zeroize::Zeroize; + + let mut m = multi_miller_loop(&[ + (&G1Affine::generator(), &G2Affine::generator().into()), + (&-G1Affine::generator(), &G2Affine::generator().into()), + ]); + m.zeroize(); + assert_eq!(m.0, MillerLoopResult::default().0); +} + +#[test] +fn tricking_miller_loop_result() { + assert_eq!( + multi_miller_loop(&[(&G1Affine::identity(), &G2Affine::generator().into())]).0, + Fp12::one() + ); + assert_eq!( + multi_miller_loop(&[(&G1Affine::generator(), &G2Affine::identity().into())]).0, + Fp12::one() + ); + assert_ne!( + multi_miller_loop(&[ + (&G1Affine::generator(), &G2Affine::generator().into()), + (&-G1Affine::generator(), &G2Affine::generator().into()) + ]) + .0, + Fp12::one() + ); + assert_eq!( + multi_miller_loop(&[ + (&G1Affine::generator(), &G2Affine::generator().into()), + (&-G1Affine::generator(), &G2Affine::generator().into()) + ]) + .final_exponentiation(), + Gt::identity() + ); +} diff --git a/constantine/bls12_381/src/scalar.rs b/constantine/bls12_381/src/scalar.rs new file mode 100644 index 000000000..c253ed427 --- /dev/null +++ b/constantine/bls12_381/src/scalar.rs @@ -0,0 +1,1278 @@ +//! This module provides an implementation of the BLS12-381 scalar field $\mathbb{F}_q$ +//! where `q = 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001` +#![allow(clippy::all)] + +use core::fmt; +use core::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}; +use rand_core::RngCore; + +use ff::{Field, PrimeField}; +use subtle::{Choice, ConditionallySelectable, ConstantTimeEq, CtOption}; + +#[cfg(feature = "bits")] +use ff::{FieldBits, PrimeFieldBits}; + +use crate::util::{adc, mac, sbb}; + +/// Represents an element of the scalar field $\mathbb{F}_q$ of the BLS12-381 elliptic +/// curve construction. +// The internal representation of this type is four 64-bit unsigned +// integers in little-endian order. `Scalar` values are always in +// Montgomery form; i.e., Scalar(a) = aR mod q, with R = 2^256. +#[derive(Clone, Copy, Eq)] +pub struct Scalar(pub [u64; 4]); + +impl fmt::Debug for Scalar { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let tmp = self.to_bytes(); + write!(f, "0x")?; + for &b in tmp.iter().rev() { + write!(f, "{:02x}", b)?; + } + Ok(()) + } +} + +impl fmt::Display for Scalar { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +impl From for Scalar { + fn from(val: u64) -> Scalar { + Scalar([val, 0, 0, 0]) * R2 + } +} + +impl ConstantTimeEq for Scalar { + fn ct_eq(&self, other: &Self) -> Choice { + self.0[0].ct_eq(&other.0[0]) + & self.0[1].ct_eq(&other.0[1]) + & self.0[2].ct_eq(&other.0[2]) + & self.0[3].ct_eq(&other.0[3]) + } +} + +impl PartialEq for Scalar { + #[inline] + fn eq(&self, other: &Self) -> bool { + bool::from(self.ct_eq(other)) + } +} + +impl ConditionallySelectable for Scalar { + fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self { + Scalar([ + u64::conditional_select(&a.0[0], &b.0[0], choice), + u64::conditional_select(&a.0[1], &b.0[1], choice), + u64::conditional_select(&a.0[2], &b.0[2], choice), + u64::conditional_select(&a.0[3], &b.0[3], choice), + ]) + } +} + +/// Constant representing the modulus +/// q = 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001 +pub const MODULUS: Scalar = Scalar([ + 0xffff_ffff_0000_0001, + 0x53bd_a402_fffe_5bfe, + 0x3339_d808_09a1_d805, + 0x73ed_a753_299d_7d48, +]); + +/// The modulus as u32 limbs. +#[cfg(all(feature = "bits", not(target_pointer_width = "64")))] +const MODULUS_LIMBS_32: [u32; 8] = [ + 0x0000_0001, + 0xffff_ffff, + 0xfffe_5bfe, + 0x53bd_a402, + 0x09a1_d805, + 0x3339_d808, + 0x299d_7d48, + 0x73ed_a753, +]; + +// The number of bits needed to represent the modulus. +const MODULUS_BITS: u32 = 255; + +// GENERATOR = 7 (multiplicative generator of r-1 order, that is also quadratic nonresidue) +const GENERATOR: Scalar = Scalar([ + 0x0000_000e_ffff_fff1, + 0x17e3_63d3_0018_9c0f, + 0xff9c_5787_6f84_57b0, + 0x3513_3220_8fc5_a8c4, +]); + +impl<'a> Neg for &'a Scalar { + type Output = Scalar; + + #[inline] + fn neg(self) -> Scalar { + self.neg() + } +} + +impl Neg for Scalar { + type Output = Scalar; + + #[inline] + fn neg(self) -> Scalar { + -&self + } +} + +impl<'a, 'b> Sub<&'b Scalar> for &'a Scalar { + type Output = Scalar; + + #[inline] + fn sub(self, rhs: &'b Scalar) -> Scalar { + self.sub(rhs) + } +} + +impl<'a, 'b> Add<&'b Scalar> for &'a Scalar { + type Output = Scalar; + + #[inline] + fn add(self, rhs: &'b Scalar) -> Scalar { + self.add(rhs) + } +} + +impl<'a, 'b> Mul<&'b Scalar> for &'a Scalar { + type Output = Scalar; + + #[inline] + fn mul(self, rhs: &'b Scalar) -> Scalar { + self.mul(rhs) + } +} + +impl_binops_additive!(Scalar, Scalar); +impl_binops_multiplicative!(Scalar, Scalar); + +/// INV = -(q^{-1} mod 2^64) mod 2^64 +const INV: u64 = 0xffff_fffe_ffff_ffff; + +/// R = 2^256 mod q +const R: Scalar = Scalar([ + 0x0000_0001_ffff_fffe, + 0x5884_b7fa_0003_4802, + 0x998c_4fef_ecbc_4ff5, + 0x1824_b159_acc5_056f, +]); + +/// R^2 = 2^512 mod q +pub const R2: Scalar = Scalar([ + 0xc999_e990_f3f2_9c6d, + 0x2b6c_edcb_8792_5c23, + 0x05d3_1496_7254_398f, + 0x0748_d9d9_9f59_ff11, +]); + +/// R^3 = 2^768 mod q +const R3: Scalar = Scalar([ + 0xc62c_1807_439b_73af, + 0x1b3e_0d18_8cf0_6990, + 0x73d1_3c71_c7b5_f418, + 0x6e2a_5bb9_c8db_33e9, +]); + +/// 2^-1 +const TWO_INV: Scalar = Scalar([ + 0x0000_0000_ffff_ffff, + 0xac42_5bfd_0001_a401, + 0xccc6_27f7_f65e_27fa, + 0x0c12_58ac_d662_82b7, +]); + +// 2^S * t = MODULUS - 1 with t odd +const S: u32 = 32; + +/// GENERATOR^t where t * 2^s + 1 = q +/// with t odd. In other words, this +/// is a 2^s root of unity. +/// +/// `GENERATOR = 7 mod q` is a generator +/// of the q - 1 order multiplicative +/// subgroup. +const ROOT_OF_UNITY: Scalar = Scalar([ + 0xb9b5_8d8c_5f0e_466a, + 0x5b1b_4c80_1819_d7ec, + 0x0af5_3ae3_52a3_1e64, + 0x5bf3_adda_19e9_b27b, +]); + +/// ROOT_OF_UNITY^-1 +const ROOT_OF_UNITY_INV: Scalar = Scalar([ + 0x4256_481a_dcf3_219a, + 0x45f3_7b7f_96b6_cad3, + 0xf9c3_f1d7_5f7a_3b27, + 0x2d2f_c049_658a_fd43, +]); + +/// GENERATOR^{2^s} where t * 2^s + 1 = q with t odd. +/// In other words, this is a t root of unity. +const DELTA: Scalar = Scalar([ + 0x70e3_10d3_d146_f96a, + 0x4b64_c089_19e2_99e6, + 0x51e1_1418_6a8b_970d, + 0x6185_d066_27c0_67cb, +]); + +impl Default for Scalar { + #[inline] + fn default() -> Self { + Self::zero() + } +} + +#[cfg(feature = "zeroize")] +impl zeroize::DefaultIsZeroes for Scalar {} + +impl Scalar { + /// Returns zero, the additive identity. + #[inline] + pub const fn zero() -> Scalar { + Scalar([0, 0, 0, 0]) + } + + /// Returns one, the multiplicative identity. + #[inline] + pub const fn one() -> Scalar { + R + } + + /// Doubles this field element. + #[inline] + pub const fn double(&self) -> Scalar { + // TODO: This can be achieved more efficiently with a bitshift. + self.add(self) + } + + /// Attempts to convert a little-endian byte representation of + /// a scalar into a `Scalar`, failing if the input is not canonical. + pub fn from_bytes(bytes: &[u8; 32]) -> CtOption { + let mut tmp = Scalar([0, 0, 0, 0]); + + tmp.0[0] = u64::from_le_bytes(<[u8; 8]>::try_from(&bytes[0..8]).unwrap()); + tmp.0[1] = u64::from_le_bytes(<[u8; 8]>::try_from(&bytes[8..16]).unwrap()); + tmp.0[2] = u64::from_le_bytes(<[u8; 8]>::try_from(&bytes[16..24]).unwrap()); + tmp.0[3] = u64::from_le_bytes(<[u8; 8]>::try_from(&bytes[24..32]).unwrap()); + + // Try to subtract the modulus + let (_, borrow) = sbb(tmp.0[0], MODULUS.0[0], 0); + let (_, borrow) = sbb(tmp.0[1], MODULUS.0[1], borrow); + let (_, borrow) = sbb(tmp.0[2], MODULUS.0[2], borrow); + let (_, borrow) = sbb(tmp.0[3], MODULUS.0[3], borrow); + + // If the element is smaller than MODULUS then the + // subtraction will underflow, producing a borrow value + // of 0xffff...ffff. Otherwise, it'll be zero. + let is_some = (borrow as u8) & 1; + + // Convert to Montgomery form by computing + // (a.R^0 * R^2) / R = a.R + tmp *= &R2; + + CtOption::new(tmp, Choice::from(is_some)) + } + + /// Converts an element of `Scalar` into a byte representation in + /// little-endian byte order. + pub fn to_bytes(&self) -> [u8; 32] { + // Turn into canonical form by computing + // (a.R) / R = a + let tmp = Scalar::montgomery_reduce(self.0[0], self.0[1], self.0[2], self.0[3], 0, 0, 0, 0); + + let mut res = [0; 32]; + res[0..8].copy_from_slice(&tmp.0[0].to_le_bytes()); + res[8..16].copy_from_slice(&tmp.0[1].to_le_bytes()); + res[16..24].copy_from_slice(&tmp.0[2].to_le_bytes()); + res[24..32].copy_from_slice(&tmp.0[3].to_le_bytes()); + + res + } + + /// Converts a 512-bit little endian integer into + /// a `Scalar` by reducing by the modulus. + pub fn from_bytes_wide(bytes: &[u8; 64]) -> Scalar { + Scalar::from_u512([ + u64::from_le_bytes(<[u8; 8]>::try_from(&bytes[0..8]).unwrap()), + u64::from_le_bytes(<[u8; 8]>::try_from(&bytes[8..16]).unwrap()), + u64::from_le_bytes(<[u8; 8]>::try_from(&bytes[16..24]).unwrap()), + u64::from_le_bytes(<[u8; 8]>::try_from(&bytes[24..32]).unwrap()), + u64::from_le_bytes(<[u8; 8]>::try_from(&bytes[32..40]).unwrap()), + u64::from_le_bytes(<[u8; 8]>::try_from(&bytes[40..48]).unwrap()), + u64::from_le_bytes(<[u8; 8]>::try_from(&bytes[48..56]).unwrap()), + u64::from_le_bytes(<[u8; 8]>::try_from(&bytes[56..64]).unwrap()), + ]) + } + + fn from_u512(limbs: [u64; 8]) -> Scalar { + // We reduce an arbitrary 512-bit number by decomposing it into two 256-bit digits + // with the higher bits multiplied by 2^256. Thus, we perform two reductions + // + // 1. the lower bits are multiplied by R^2, as normal + // 2. the upper bits are multiplied by R^2 * 2^256 = R^3 + // + // and computing their sum in the field. It remains to see that arbitrary 256-bit + // numbers can be placed into Montgomery form safely using the reduction. The + // reduction works so long as the product is less than R=2^256 multiplied by + // the modulus. This holds because for any `c` smaller than the modulus, we have + // that (2^256 - 1)*c is an acceptable product for the reduction. Therefore, the + // reduction always works so long as `c` is in the field; in this case it is either the + // constant `R2` or `R3`. + let d0 = Scalar([limbs[0], limbs[1], limbs[2], limbs[3]]); + let d1 = Scalar([limbs[4], limbs[5], limbs[6], limbs[7]]); + // Convert to Montgomery form + d0 * R2 + d1 * R3 + } + + /// Converts from an integer represented in little endian + /// into its (congruent) `Scalar` representation. + pub const fn from_raw(val: [u64; 4]) -> Self { + (&Scalar(val)).mul(&R2) + } + + /// Squares this element. + #[inline] + pub const fn square(&self) -> Scalar { + let (r1, carry) = mac(0, self.0[0], self.0[1], 0); + let (r2, carry) = mac(0, self.0[0], self.0[2], carry); + let (r3, r4) = mac(0, self.0[0], self.0[3], carry); + + let (r3, carry) = mac(r3, self.0[1], self.0[2], 0); + let (r4, r5) = mac(r4, self.0[1], self.0[3], carry); + + let (r5, r6) = mac(r5, self.0[2], self.0[3], 0); + + let r7 = r6 >> 63; + let r6 = (r6 << 1) | (r5 >> 63); + let r5 = (r5 << 1) | (r4 >> 63); + let r4 = (r4 << 1) | (r3 >> 63); + let r3 = (r3 << 1) | (r2 >> 63); + let r2 = (r2 << 1) | (r1 >> 63); + let r1 = r1 << 1; + + let (r0, carry) = mac(0, self.0[0], self.0[0], 0); + let (r1, carry) = adc(0, r1, carry); + let (r2, carry) = mac(r2, self.0[1], self.0[1], carry); + let (r3, carry) = adc(0, r3, carry); + let (r4, carry) = mac(r4, self.0[2], self.0[2], carry); + let (r5, carry) = adc(0, r5, carry); + let (r6, carry) = mac(r6, self.0[3], self.0[3], carry); + let (r7, _) = adc(0, r7, carry); + + Scalar::montgomery_reduce(r0, r1, r2, r3, r4, r5, r6, r7) + } + + /// Exponentiates `self` by `by`, where `by` is a + /// little-endian order integer exponent. + pub fn pow(&self, by: &[u64; 4]) -> Self { + let mut res = Self::one(); + for e in by.iter().rev() { + for i in (0..64).rev() { + res = res.square(); + let mut tmp = res; + tmp *= self; + res.conditional_assign(&tmp, (((*e >> i) & 0x1) as u8).into()); + } + } + res + } + + /// Exponentiates `self` by `by`, where `by` is a + /// little-endian order integer exponent. + /// + /// **This operation is variable time with respect + /// to the exponent.** If the exponent is fixed, + /// this operation is effectively constant time. + pub fn pow_vartime(&self, by: &[u64; 4]) -> Self { + let mut res = Self::one(); + for e in by.iter().rev() { + for i in (0..64).rev() { + res = res.square(); + + if ((*e >> i) & 1) == 1 { + res.mul_assign(self); + } + } + } + res + } + + /// Computes the multiplicative inverse of this element, + /// failing if the element is zero. + pub fn invert(&self) -> CtOption { + #[inline(always)] + fn square_assign_multi(n: &mut Scalar, num_times: usize) { + for _ in 0..num_times { + *n = n.square(); + } + } + // found using https://github.com/kwantam/addchain + let mut t0 = self.square(); + let mut t1 = t0 * self; + let mut t16 = t0.square(); + let mut t6 = t16.square(); + let mut t5 = t6 * t0; + t0 = t6 * t16; + let mut t12 = t5 * t16; + let mut t2 = t6.square(); + let mut t7 = t5 * t6; + let mut t15 = t0 * t5; + let mut t17 = t12.square(); + t1 *= t17; + let mut t3 = t7 * t2; + let t8 = t1 * t17; + let t4 = t8 * t2; + let t9 = t8 * t7; + t7 = t4 * t5; + let t11 = t4 * t17; + t5 = t9 * t17; + let t14 = t7 * t15; + let t13 = t11 * t12; + t12 = t11 * t17; + t15 *= &t12; + t16 *= &t15; + t3 *= &t16; + t17 *= &t3; + t0 *= &t17; + t6 *= &t0; + t2 *= &t6; + square_assign_multi(&mut t0, 8); + t0 *= &t17; + square_assign_multi(&mut t0, 9); + t0 *= &t16; + square_assign_multi(&mut t0, 9); + t0 *= &t15; + square_assign_multi(&mut t0, 9); + t0 *= &t15; + square_assign_multi(&mut t0, 7); + t0 *= &t14; + square_assign_multi(&mut t0, 7); + t0 *= &t13; + square_assign_multi(&mut t0, 10); + t0 *= &t12; + square_assign_multi(&mut t0, 9); + t0 *= &t11; + square_assign_multi(&mut t0, 8); + t0 *= &t8; + square_assign_multi(&mut t0, 8); + t0 *= self; + square_assign_multi(&mut t0, 14); + t0 *= &t9; + square_assign_multi(&mut t0, 10); + t0 *= &t8; + square_assign_multi(&mut t0, 15); + t0 *= &t7; + square_assign_multi(&mut t0, 10); + t0 *= &t6; + square_assign_multi(&mut t0, 8); + t0 *= &t5; + square_assign_multi(&mut t0, 16); + t0 *= &t3; + square_assign_multi(&mut t0, 8); + t0 *= &t2; + square_assign_multi(&mut t0, 7); + t0 *= &t4; + square_assign_multi(&mut t0, 9); + t0 *= &t2; + square_assign_multi(&mut t0, 8); + t0 *= &t3; + square_assign_multi(&mut t0, 8); + t0 *= &t2; + square_assign_multi(&mut t0, 8); + t0 *= &t2; + square_assign_multi(&mut t0, 8); + t0 *= &t2; + square_assign_multi(&mut t0, 8); + t0 *= &t3; + square_assign_multi(&mut t0, 8); + t0 *= &t2; + square_assign_multi(&mut t0, 8); + t0 *= &t2; + square_assign_multi(&mut t0, 5); + t0 *= &t1; + square_assign_multi(&mut t0, 5); + t0 *= &t1; + + CtOption::new(t0, !self.ct_eq(&Self::zero())) + } + + #[inline(always)] + pub const fn montgomery_reduce( + r0: u64, + r1: u64, + r2: u64, + r3: u64, + r4: u64, + r5: u64, + r6: u64, + r7: u64, + ) -> Self { + // The Montgomery reduction here is based on Algorithm 14.32 in + // Handbook of Applied Cryptography + // . + + let k = r0.wrapping_mul(INV); + let (_, carry) = mac(r0, k, MODULUS.0[0], 0); + let (r1, carry) = mac(r1, k, MODULUS.0[1], carry); + let (r2, carry) = mac(r2, k, MODULUS.0[2], carry); + let (r3, carry) = mac(r3, k, MODULUS.0[3], carry); + let (r4, carry2) = adc(r4, 0, carry); + + let k = r1.wrapping_mul(INV); + let (_, carry) = mac(r1, k, MODULUS.0[0], 0); + let (r2, carry) = mac(r2, k, MODULUS.0[1], carry); + let (r3, carry) = mac(r3, k, MODULUS.0[2], carry); + let (r4, carry) = mac(r4, k, MODULUS.0[3], carry); + let (r5, carry2) = adc(r5, carry2, carry); + + let k = r2.wrapping_mul(INV); + let (_, carry) = mac(r2, k, MODULUS.0[0], 0); + let (r3, carry) = mac(r3, k, MODULUS.0[1], carry); + let (r4, carry) = mac(r4, k, MODULUS.0[2], carry); + let (r5, carry) = mac(r5, k, MODULUS.0[3], carry); + let (r6, carry2) = adc(r6, carry2, carry); + + let k = r3.wrapping_mul(INV); + let (_, carry) = mac(r3, k, MODULUS.0[0], 0); + let (r4, carry) = mac(r4, k, MODULUS.0[1], carry); + let (r5, carry) = mac(r5, k, MODULUS.0[2], carry); + let (r6, carry) = mac(r6, k, MODULUS.0[3], carry); + let (r7, _) = adc(r7, carry2, carry); + + // Result may be within MODULUS of the correct value + (&Scalar([r4, r5, r6, r7])).sub(&MODULUS) + } + + /// Multiplies `rhs` by `self`, returning the result. + #[inline] + pub const fn mul(&self, rhs: &Self) -> Self { + // Schoolbook multiplication + + let (r0, carry) = mac(0, self.0[0], rhs.0[0], 0); + let (r1, carry) = mac(0, self.0[0], rhs.0[1], carry); + let (r2, carry) = mac(0, self.0[0], rhs.0[2], carry); + let (r3, r4) = mac(0, self.0[0], rhs.0[3], carry); + + let (r1, carry) = mac(r1, self.0[1], rhs.0[0], 0); + let (r2, carry) = mac(r2, self.0[1], rhs.0[1], carry); + let (r3, carry) = mac(r3, self.0[1], rhs.0[2], carry); + let (r4, r5) = mac(r4, self.0[1], rhs.0[3], carry); + + let (r2, carry) = mac(r2, self.0[2], rhs.0[0], 0); + let (r3, carry) = mac(r3, self.0[2], rhs.0[1], carry); + let (r4, carry) = mac(r4, self.0[2], rhs.0[2], carry); + let (r5, r6) = mac(r5, self.0[2], rhs.0[3], carry); + + let (r3, carry) = mac(r3, self.0[3], rhs.0[0], 0); + let (r4, carry) = mac(r4, self.0[3], rhs.0[1], carry); + let (r5, carry) = mac(r5, self.0[3], rhs.0[2], carry); + let (r6, r7) = mac(r6, self.0[3], rhs.0[3], carry); + + Scalar::montgomery_reduce(r0, r1, r2, r3, r4, r5, r6, r7) + } + + /// Subtracts `rhs` from `self`, returning the result. + #[inline] + pub const fn sub(&self, rhs: &Self) -> Self { + let (d0, borrow) = sbb(self.0[0], rhs.0[0], 0); + let (d1, borrow) = sbb(self.0[1], rhs.0[1], borrow); + let (d2, borrow) = sbb(self.0[2], rhs.0[2], borrow); + let (d3, borrow) = sbb(self.0[3], rhs.0[3], borrow); + + // If underflow occurred on the final limb, borrow = 0xfff...fff, otherwise + // borrow = 0x000...000. Thus, we use it as a mask to conditionally add the modulus. + let (d0, carry) = adc(d0, MODULUS.0[0] & borrow, 0); + let (d1, carry) = adc(d1, MODULUS.0[1] & borrow, carry); + let (d2, carry) = adc(d2, MODULUS.0[2] & borrow, carry); + let (d3, _) = adc(d3, MODULUS.0[3] & borrow, carry); + + Scalar([d0, d1, d2, d3]) + } + + /// Adds `rhs` to `self`, returning the result. + #[inline] + pub const fn add(&self, rhs: &Self) -> Self { + let (d0, carry) = adc(self.0[0], rhs.0[0], 0); + let (d1, carry) = adc(self.0[1], rhs.0[1], carry); + let (d2, carry) = adc(self.0[2], rhs.0[2], carry); + let (d3, _) = adc(self.0[3], rhs.0[3], carry); + + // Attempt to subtract the modulus, to ensure the value + // is smaller than the modulus. + (&Scalar([d0, d1, d2, d3])).sub(&MODULUS) + } + + /// Negates `self`. + #[inline] + pub const fn neg(&self) -> Self { + // Subtract `self` from `MODULUS` to negate. Ignore the final + // borrow because it cannot underflow; self is guaranteed to + // be in the field. + let (d0, borrow) = sbb(MODULUS.0[0], self.0[0], 0); + let (d1, borrow) = sbb(MODULUS.0[1], self.0[1], borrow); + let (d2, borrow) = sbb(MODULUS.0[2], self.0[2], borrow); + let (d3, _) = sbb(MODULUS.0[3], self.0[3], borrow); + + // `tmp` could be `MODULUS` if `self` was zero. Create a mask that is + // zero if `self` was zero, and `u64::max_value()` if self was nonzero. + let mask = (((self.0[0] | self.0[1] | self.0[2] | self.0[3]) == 0) as u64).wrapping_sub(1); + + Scalar([d0 & mask, d1 & mask, d2 & mask, d3 & mask]) + } +} + +impl From for [u8; 32] { + fn from(value: Scalar) -> [u8; 32] { + value.to_bytes() + } +} + +impl<'a> From<&'a Scalar> for [u8; 32] { + fn from(value: &'a Scalar) -> [u8; 32] { + value.to_bytes() + } +} + +impl Field for Scalar { + const ZERO: Self = Self::zero(); + const ONE: Self = Self::one(); + + fn random(mut rng: impl RngCore) -> Self { + let mut buf = [0; 64]; + rng.fill_bytes(&mut buf); + Self::from_bytes_wide(&buf) + } + + #[must_use] + fn square(&self) -> Self { + self.square() + } + + #[must_use] + fn double(&self) -> Self { + self.double() + } + + fn invert(&self) -> CtOption { + self.invert() + } + + fn sqrt_ratio(num: &Self, div: &Self) -> (Choice, Self) { + ff::helpers::sqrt_ratio_generic(num, div) + } + + fn sqrt(&self) -> CtOption { + // (t - 1) // 2 = 6104339283789297388802252303364915521546564123189034618274734669823 + ff::helpers::sqrt_tonelli_shanks( + self, + &[ + 0x7fff_2dff_7fff_ffff, + 0x04d0_ec02_a9de_d201, + 0x94ce_bea4_199c_ec04, + 0x0000_0000_39f6_d3a9, + ], + ) + } + + fn is_zero_vartime(&self) -> bool { + self.0 == Self::zero().0 + } +} + +impl PrimeField for Scalar { + type Repr = [u8; 32]; + + fn from_repr(r: Self::Repr) -> CtOption { + Self::from_bytes(&r) + } + + fn to_repr(&self) -> Self::Repr { + self.to_bytes() + } + + fn is_odd(&self) -> Choice { + Choice::from(self.to_bytes()[0] & 1) + } + + const MODULUS: &'static str = + "0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001"; + const NUM_BITS: u32 = MODULUS_BITS; + const CAPACITY: u32 = Self::NUM_BITS - 1; + const TWO_INV: Self = TWO_INV; + const MULTIPLICATIVE_GENERATOR: Self = GENERATOR; + const S: u32 = S; + const ROOT_OF_UNITY: Self = ROOT_OF_UNITY; + const ROOT_OF_UNITY_INV: Self = ROOT_OF_UNITY_INV; + const DELTA: Self = DELTA; +} + +#[cfg(all(feature = "bits", not(target_pointer_width = "64")))] +type ReprBits = [u32; 8]; + +#[cfg(all(feature = "bits", target_pointer_width = "64"))] +type ReprBits = [u64; 4]; + +#[cfg(feature = "bits")] +impl PrimeFieldBits for Scalar { + type ReprBits = ReprBits; + + fn to_le_bits(&self) -> FieldBits { + let bytes = self.to_bytes(); + + #[cfg(not(target_pointer_width = "64"))] + let limbs = [ + u32::from_le_bytes(bytes[0..4].try_into().unwrap()), + u32::from_le_bytes(bytes[4..8].try_into().unwrap()), + u32::from_le_bytes(bytes[8..12].try_into().unwrap()), + u32::from_le_bytes(bytes[12..16].try_into().unwrap()), + u32::from_le_bytes(bytes[16..20].try_into().unwrap()), + u32::from_le_bytes(bytes[20..24].try_into().unwrap()), + u32::from_le_bytes(bytes[24..28].try_into().unwrap()), + u32::from_le_bytes(bytes[28..32].try_into().unwrap()), + ]; + + #[cfg(target_pointer_width = "64")] + let limbs = [ + u64::from_le_bytes(bytes[0..8].try_into().unwrap()), + u64::from_le_bytes(bytes[8..16].try_into().unwrap()), + u64::from_le_bytes(bytes[16..24].try_into().unwrap()), + u64::from_le_bytes(bytes[24..32].try_into().unwrap()), + ]; + + FieldBits::new(limbs) + } + + fn char_le_bits() -> FieldBits { + #[cfg(not(target_pointer_width = "64"))] + { + FieldBits::new(MODULUS_LIMBS_32) + } + + #[cfg(target_pointer_width = "64")] + FieldBits::new(MODULUS.0) + } +} + +impl core::iter::Sum for Scalar +where + T: core::borrow::Borrow, +{ + fn sum(iter: I) -> Self + where + I: Iterator, + { + iter.fold(Self::zero(), |acc, item| acc + item.borrow()) + } +} + +impl core::iter::Product for Scalar +where + T: core::borrow::Borrow, +{ + fn product(iter: I) -> Self + where + I: Iterator, + { + iter.fold(Self::one(), |acc, item| acc * item.borrow()) + } +} + +#[test] +fn test_constants() { + assert_eq!( + Scalar::MODULUS, + "0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001", + ); + + assert_eq!(Scalar::from(2) * Scalar::TWO_INV, Scalar::ONE); + + assert_eq!( + Scalar::ROOT_OF_UNITY * Scalar::ROOT_OF_UNITY_INV, + Scalar::ONE, + ); + + // ROOT_OF_UNITY^{2^s} mod m == 1 + assert_eq!( + Scalar::ROOT_OF_UNITY.pow(&[1u64 << Scalar::S, 0, 0, 0]), + Scalar::ONE, + ); + + // DELTA^{t} mod m == 1 + assert_eq!( + Scalar::DELTA.pow(&[ + 0xfffe_5bfe_ffff_ffff, + 0x09a1_d805_53bd_a402, + 0x299d_7d48_3339_d808, + 0x0000_0000_73ed_a753, + ]), + Scalar::ONE, + ); +} + +#[test] +fn test_inv() { + // Compute -(q^{-1} mod 2^64) mod 2^64 by exponentiating + // by totient(2**64) - 1 + + let mut inv = 1u64; + for _ in 0..63 { + inv = inv.wrapping_mul(inv); + inv = inv.wrapping_mul(MODULUS.0[0]); + } + inv = inv.wrapping_neg(); + + assert_eq!(inv, INV); +} + +#[cfg(feature = "std")] +#[test] +fn test_debug() { + assert_eq!( + format!("{:?}", Scalar::zero()), + "0x0000000000000000000000000000000000000000000000000000000000000000" + ); + assert_eq!( + format!("{:?}", Scalar::one()), + "0x0000000000000000000000000000000000000000000000000000000000000001" + ); + assert_eq!( + format!("{:?}", R2), + "0x1824b159acc5056f998c4fefecbc4ff55884b7fa0003480200000001fffffffe" + ); +} + +#[test] +fn test_equality() { + assert_eq!(Scalar::zero(), Scalar::zero()); + assert_eq!(Scalar::one(), Scalar::one()); + assert_eq!(R2, R2); + + assert!(Scalar::zero() != Scalar::one()); + assert!(Scalar::one() != R2); +} + +#[test] +fn test_to_bytes() { + assert_eq!( + Scalar::zero().to_bytes(), + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0 + ] + ); + + assert_eq!( + Scalar::one().to_bytes(), + [ + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0 + ] + ); + + assert_eq!( + R2.to_bytes(), + [ + 254, 255, 255, 255, 1, 0, 0, 0, 2, 72, 3, 0, 250, 183, 132, 88, 245, 79, 188, 236, 239, + 79, 140, 153, 111, 5, 197, 172, 89, 177, 36, 24 + ] + ); + + assert_eq!( + (-&Scalar::one()).to_bytes(), + [ + 0, 0, 0, 0, 255, 255, 255, 255, 254, 91, 254, 255, 2, 164, 189, 83, 5, 216, 161, 9, 8, + 216, 57, 51, 72, 125, 157, 41, 83, 167, 237, 115 + ] + ); +} + +#[test] +fn test_from_bytes() { + assert_eq!( + Scalar::from_bytes(&[ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0 + ]) + .unwrap(), + Scalar::zero() + ); + + assert_eq!( + Scalar::from_bytes(&[ + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0 + ]) + .unwrap(), + Scalar::one() + ); + + assert_eq!( + Scalar::from_bytes(&[ + 254, 255, 255, 255, 1, 0, 0, 0, 2, 72, 3, 0, 250, 183, 132, 88, 245, 79, 188, 236, 239, + 79, 140, 153, 111, 5, 197, 172, 89, 177, 36, 24 + ]) + .unwrap(), + R2 + ); + + // -1 should work + assert!(bool::from( + Scalar::from_bytes(&[ + 0, 0, 0, 0, 255, 255, 255, 255, 254, 91, 254, 255, 2, 164, 189, 83, 5, 216, 161, 9, 8, + 216, 57, 51, 72, 125, 157, 41, 83, 167, 237, 115 + ]) + .is_some() + )); + + // modulus is invalid + assert!(bool::from( + Scalar::from_bytes(&[ + 1, 0, 0, 0, 255, 255, 255, 255, 254, 91, 254, 255, 2, 164, 189, 83, 5, 216, 161, 9, 8, + 216, 57, 51, 72, 125, 157, 41, 83, 167, 237, 115 + ]) + .is_none() + )); + + // Anything larger than the modulus is invalid + assert!(bool::from( + Scalar::from_bytes(&[ + 2, 0, 0, 0, 255, 255, 255, 255, 254, 91, 254, 255, 2, 164, 189, 83, 5, 216, 161, 9, 8, + 216, 57, 51, 72, 125, 157, 41, 83, 167, 237, 115 + ]) + .is_none() + )); + assert!(bool::from( + Scalar::from_bytes(&[ + 1, 0, 0, 0, 255, 255, 255, 255, 254, 91, 254, 255, 2, 164, 189, 83, 5, 216, 161, 9, 8, + 216, 58, 51, 72, 125, 157, 41, 83, 167, 237, 115 + ]) + .is_none() + )); + assert!(bool::from( + Scalar::from_bytes(&[ + 1, 0, 0, 0, 255, 255, 255, 255, 254, 91, 254, 255, 2, 164, 189, 83, 5, 216, 161, 9, 8, + 216, 57, 51, 72, 125, 157, 41, 83, 167, 237, 116 + ]) + .is_none() + )); +} + +#[test] +fn test_from_u512_zero() { + assert_eq!( + Scalar::zero(), + Scalar::from_u512([ + MODULUS.0[0], + MODULUS.0[1], + MODULUS.0[2], + MODULUS.0[3], + 0, + 0, + 0, + 0 + ]) + ); +} + +#[test] +fn test_from_u512_r() { + assert_eq!(R, Scalar::from_u512([1, 0, 0, 0, 0, 0, 0, 0])); +} + +#[test] +fn test_from_u512_r2() { + assert_eq!(R2, Scalar::from_u512([0, 0, 0, 0, 1, 0, 0, 0])); +} + +#[test] +fn test_from_u512_max() { + let max_u64 = 0xffff_ffff_ffff_ffff; + assert_eq!( + R3 - R, + Scalar::from_u512([max_u64, max_u64, max_u64, max_u64, max_u64, max_u64, max_u64, max_u64]) + ); +} + +#[test] +fn test_from_bytes_wide_r2() { + assert_eq!( + R2, + Scalar::from_bytes_wide(&[ + 254, 255, 255, 255, 1, 0, 0, 0, 2, 72, 3, 0, 250, 183, 132, 88, 245, 79, 188, 236, 239, + 79, 140, 153, 111, 5, 197, 172, 89, 177, 36, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]) + ); +} + +#[test] +fn test_from_bytes_wide_negative_one() { + assert_eq!( + -&Scalar::one(), + Scalar::from_bytes_wide(&[ + 0, 0, 0, 0, 255, 255, 255, 255, 254, 91, 254, 255, 2, 164, 189, 83, 5, 216, 161, 9, 8, + 216, 57, 51, 72, 125, 157, 41, 83, 167, 237, 115, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]) + ); +} + +#[test] +fn test_from_bytes_wide_maximum() { + assert_eq!( + Scalar([ + 0xc62c_1805_439b_73b1, + 0xc2b9_551e_8ced_218e, + 0xda44_ec81_daf9_a422, + 0x5605_aa60_1c16_2e79, + ]), + Scalar::from_bytes_wide(&[0xff; 64]) + ); +} + +#[test] +fn test_zero() { + assert_eq!(Scalar::zero(), -&Scalar::zero()); + assert_eq!(Scalar::zero(), Scalar::zero() + Scalar::zero()); + assert_eq!(Scalar::zero(), Scalar::zero() - Scalar::zero()); + assert_eq!(Scalar::zero(), Scalar::zero() * Scalar::zero()); +} + +#[cfg(test)] +const LARGEST: Scalar = Scalar([ + 0xffff_ffff_0000_0000, + 0x53bd_a402_fffe_5bfe, + 0x3339_d808_09a1_d805, + 0x73ed_a753_299d_7d48, +]); + +#[test] +fn test_addition() { + let mut tmp = LARGEST; + tmp += &LARGEST; + + assert_eq!( + tmp, + Scalar([ + 0xffff_fffe_ffff_ffff, + 0x53bd_a402_fffe_5bfe, + 0x3339_d808_09a1_d805, + 0x73ed_a753_299d_7d48, + ]) + ); + + let mut tmp = LARGEST; + tmp += &Scalar([1, 0, 0, 0]); + + assert_eq!(tmp, Scalar::zero()); +} + +#[test] +fn test_negation() { + let tmp = -&LARGEST; + + assert_eq!(tmp, Scalar([1, 0, 0, 0])); + + let tmp = -&Scalar::zero(); + assert_eq!(tmp, Scalar::zero()); + let tmp = -&Scalar([1, 0, 0, 0]); + assert_eq!(tmp, LARGEST); +} + +#[test] +fn test_subtraction() { + let mut tmp = LARGEST; + tmp -= &LARGEST; + + assert_eq!(tmp, Scalar::zero()); + + let mut tmp = Scalar::zero(); + tmp -= &LARGEST; + + let mut tmp2 = MODULUS; + tmp2 -= &LARGEST; + + assert_eq!(tmp, tmp2); +} + +#[test] +fn test_multiplication() { + let mut cur = LARGEST; + + for _ in 0..100 { + let mut tmp = cur; + tmp *= &cur; + + let mut tmp2 = Scalar::zero(); + for b in cur + .to_bytes() + .iter() + .rev() + .flat_map(|byte| (0..8).rev().map(move |i| ((byte >> i) & 1u8) == 1u8)) + { + let tmp3 = tmp2; + tmp2.add_assign(&tmp3); + + if b { + tmp2.add_assign(&cur); + } + } + + assert_eq!(tmp, tmp2); + + cur.add_assign(&LARGEST); + } +} + +#[test] +fn test_squaring() { + let mut cur = LARGEST; + + for _ in 0..100 { + let mut tmp = cur; + tmp = tmp.square(); + + let mut tmp2 = Scalar::zero(); + for b in cur + .to_bytes() + .iter() + .rev() + .flat_map(|byte| (0..8).rev().map(move |i| ((byte >> i) & 1u8) == 1u8)) + { + let tmp3 = tmp2; + tmp2.add_assign(&tmp3); + + if b { + tmp2.add_assign(&cur); + } + } + + assert_eq!(tmp, tmp2); + + cur.add_assign(&LARGEST); + } +} + +#[test] +fn test_inversion() { + assert!(bool::from(Scalar::zero().invert().is_none())); + assert_eq!(Scalar::one().invert().unwrap(), Scalar::one()); + assert_eq!((-&Scalar::one()).invert().unwrap(), -&Scalar::one()); + + let mut tmp = R2; + + for _ in 0..100 { + let mut tmp2 = tmp.invert().unwrap(); + tmp2.mul_assign(&tmp); + + assert_eq!(tmp2, Scalar::one()); + + tmp.add_assign(&R2); + } +} + +#[test] +fn test_invert_is_pow() { + let q_minus_2 = [ + 0xffff_fffe_ffff_ffff, + 0x53bd_a402_fffe_5bfe, + 0x3339_d808_09a1_d805, + 0x73ed_a753_299d_7d48, + ]; + + let mut r1 = R; + let mut r2 = R; + let mut r3 = R; + + for _ in 0..100 { + r1 = r1.invert().unwrap(); + r2 = r2.pow_vartime(&q_minus_2); + r3 = r3.pow(&q_minus_2); + + assert_eq!(r1, r2); + assert_eq!(r2, r3); + // Add R so we check something different next time around + r1.add_assign(&R); + r2 = r1; + r3 = r1; + } +} + +#[test] +fn test_sqrt() { + { + assert_eq!(Scalar::zero().sqrt().unwrap(), Scalar::zero()); + } + + let mut square = Scalar([ + 0x46cd_85a5_f273_077e, + 0x1d30_c47d_d68f_c735, + 0x77f6_56f6_0bec_a0eb, + 0x494a_a01b_df32_468d, + ]); + + let mut none_count = 0; + + for _ in 0..100 { + let square_root = square.sqrt(); + if bool::from(square_root.is_none()) { + none_count += 1; + } else { + assert_eq!(square_root.unwrap() * square_root.unwrap(), square); + } + square -= Scalar::one(); + } + + assert_eq!(49, none_count); +} + +#[test] +fn test_from_raw() { + assert_eq!( + Scalar::from_raw([ + 0x0001_ffff_fffd, + 0x5884_b7fa_0003_4802, + 0x998c_4fef_ecbc_4ff5, + 0x1824_b159_acc5_056f, + ]), + Scalar::from_raw([0xffff_ffff_ffff_ffff; 4]) + ); + + assert_eq!(Scalar::from_raw(MODULUS.0), Scalar::zero()); + + assert_eq!(Scalar::from_raw([1, 0, 0, 0]), R); +} + +#[test] +fn test_double() { + let a = Scalar::from_raw([ + 0x1fff_3231_233f_fffd, + 0x4884_b7fa_0003_4802, + 0x998c_4fef_ecbc_4ff3, + 0x1824_b159_acc5_0562, + ]); + + assert_eq!(a.double(), a + a); +} + +#[cfg(feature = "zeroize")] +#[test] +fn test_zeroize() { + use zeroize::Zeroize; + + let mut a = Scalar::from_raw([ + 0x1fff_3231_233f_fffd, + 0x4884_b7fa_0003_4802, + 0x998c_4fef_ecbc_4ff3, + 0x1824_b159_acc5_0562, + ]); + a.zeroize(); + assert!(bool::from(a.is_zero())); +} diff --git a/constantine/bls12_381/src/tests/g1_compressed_valid_test_vectors.dat b/constantine/bls12_381/src/tests/g1_compressed_valid_test_vectors.dat new file mode 100644 index 0000000000000000000000000000000000000000..ea8cd67652d133010e79df8488452450b6f5cb17 GIT binary patch literal 48000 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 Rm?nepfGSPSF*XppvfIk?3d#Tg literal 0 HcmV?d00001 diff --git a/constantine/bls12_381/src/tests/g1_uncompressed_valid_test_vectors.dat b/constantine/bls12_381/src/tests/g1_uncompressed_valid_test_vectors.dat new file mode 100644 index 0000000000000000000000000000000000000000..86abfba945c7b701750d637bffe6701330e10e88 GIT binary patch literal 96000 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%0A9l>2- zfDnX>M_o9bLjV&-?O#zHse{a2_A72l!z>Q&=aQsA2<8g&6cA#;a1-!$bcL;kGU73&locfS_AQGns@ zhIT6w1QGxGe7RQ74q{Ve>7vzd0rL&ORpxGi335=!%;(U3LNV5U>+w}8Q9IuG+}2y{ zp0wTqV|T(pc}?0kWh9XOJ97!W#rP<9f;V=kLCubnhkOoR3stSoy05P=O=MuJBGW6) zen#m+>`jO=>ZdiZi6aKCo;9^z<0z2bFWPkDBp(Iq5o|I*BOOOZy*%4&9Y)E?aaM$Z zI)%dU1Q>h)g*Oa2nK6EhXH( z#90Qtu%I;BwnE@sv*bb6e_-Ic^gp2#4m2MEiY!Y6^Yn@g++N|wf?dtT0h<6$z;g(+ z#Ba+s1DuZ%Jgt6{JhQ`H`P^^eXNY&NJiMWpXe^litd4^Jt=m;)F2%>L0JZ|&4w$fs zCG&aFvcwKB8DC)tK|q?ba!S+k(Q{Eg9f(YeC$IDZ%>z^So?*wdt=kBrMeLAcbt49E zdsCW*+yuDR5eT)>=aJQXg=%%+GYv98q{{|MV8cF-I zhu6O+j>VP=5t6Vf_?-lijWa}bPqQdXQ=u1Y_oRxtFo~U!;Cjv8OS<4X>O^wLC>5}C zYZ5`U7k@u%uftvWn(Rz@x@c3~oi7Gm{c)14!j|%Gbgol;*riM;MGqL&dF^#7+2F<8 zvW6}e4pQiNS=S#!Iz3NZqmv0xL&}8uGi3hrQ9gLz0mmGSU{ykm(O0j?mc|I@4;$$! z=#MP_XtmdG7xd-~bhia8(Ob34o85fxC$nTjRxynasWTswi0#2EAWkl|Z>;5DPE0$w zV;=RrLyz}@6(bodQvo+tw01xD&)z5Be=P}Yd@>Ds`XL|Wpmf$Xk`@$0o3?9X?G`Us zWkB~{hMNhB=jBZzo2(X1Gm^_nx!&t$5RBG`8`D6`$rleP)L<$sCulF7FQcu18Ri7G z0csS!!2=QiJ&~Qb)q|he{rk@(EvAoWE5cHVa3T78xORjj05l!*EuS?eNYwVsBG3~Y z+lTTdt#Isa5A=S7AV?hu4p)^FL#=>B5vR9}!t&{yz4Asq+Bw#~D%o5IFyRXsO>b>h zj_Y6~gy<9w#>W{3Fu88^{h@?|xUuEL4I&y!_nshy_@qCwc3U3l^AcsGIE4WJxllcF8Kk)9k@3A9!5Ago_jKl_NWOk50Es>DSRam4FPC zA%|PVk9(^}34{oN`}^VR6EZgLt)E7n-j)OKZblU-qHj6m#1=mnfwPDjNwhoTKRw<^ zY7=*S_q}F?swB(s=sWhDU`jxURw&<{uwo8;v=fC6%WoYm?U+&Hg4so={@@>vRTxZr z2p0eVbCgVLY zGc@NYS$XAPxcLR$w_f@Mx~g)0Wt$1oXtoCU5Gi?X5NlQcjYs{rN_``8hTrPl=FBG5 zJysx2|78UB2;>mHU^-}X!#v9i6p^W+XBF#LS5uuRM}NreP21oP7RoDGL~419YkUPk zTt5IgXkd>&6i1OOwl(qq?OO}N^R41@Ys|1(aL7D;^;QC+azAUAK;o-gZ8sZ)r%BQygWP@;plmUsX+DJ4T$j6@NWoo5cW-8{?&7F;-D)Z;CG z(3&_t0w($fke%O7X*dG?Ttg&4eTKs=@%_X@h*LI=w$1}3EC*JNNyn3RCS%^M?9GaP z_K~(yEF5yw$CFb|6gH9H_RthB&xfVZL0OWzI@$*Q@?p(e%++jiyP?0!Q7n5!0aAn8 z>U3DvqdKwe`{?|iH{lJ2-c$O@IE9>I4vkF3bKxq^^xEftJ z!WLzLr%U?(l<=D+b7K?8p%|iDU_on6Z=MUs%SuuUBC(h+O>3lmKS)msn9u<3*!2OB zQG|4t@=w_Znsg6%&yu`Zs6pkoh2D5M9Gx`b-H_(tL^RifJ0Cm9-NF=DtCO(h!7480 z*84FtJV*y>Mxo=1isbvPoVhb=uG05Fupg_pgd;lQ_)q+oD(}C=MUH_~k*j00FUx24 zx623p^6%580rkAB+UEeHK?A)}JW6aY<<3%5qlSl=K8Sq90~5kM$}&H))8l+|j$ea?jC-Y7*i%-^&2^KgQ8$xo55e;2|nrCyhk6BeGRMu38X|;5Zicig?l5 z1q_bCCDYo%FkyJq^+(ys3)PD`g`y8{?@)crf$$L50%F;#t7ru0zqXeBPmIA{#Jc^7 z{-Zbxh@Cfnt^$dPo^}dj7BFM{dZJIDDox7A<-UtVd4LfkG5}AW%z)BZ{#<@)54tof zlZgsmpv>B2E_xgU0e*#_bC0WU zyix7E^RO!tb~9}sf;k>ZS0n7cGkyZ0BdFLLQYgS!VZfbL0O^d`VL%CH6=#C|pWZg7 zMo)qgY21wxGPG(n=yvkWQUCyLPnQEk3pQKa(MN0vdQsPNMlcObq_SjCTnP!~pI$OMCB+d(%ua4OML0}bLhKRh&IPa{1U_B( z8%YrD@?pDb`mO}Y3=}CX{Zi6>4pwWwsVXY#u(Z6>JtS?8>qHf7e=pQ4sIR!=CYf?f zHebbq=>!xa$k@b;t*;h}R+VPY4r|W-yq&eKs$=}cc2m3)PK^hK{k#mx?GSO#6@}(N z>fi{mQ**;n;%;5Vy1G@%E*=3-gPM6bc245n_Hn&>#cV%|Oq??hYUgzT)AtdMw&)c| z&n5RTE35{3d*A24upegV%C#a5=WxncBdq_UWg zy^FDUkggg~q|^I;PF@$v23{xC>V|y*VNgabeQdSulZXMonky602E+sfZbStc*rDcE z@=4jM;S(i!WCXhej2DujFN)pwswu^SD3i#{K!qoHJ!%i#1b1S)o(}$vev(;TK97Fw z_x-w@Z%5yEupMjDDIeyD@Yhz zn#G%Qa=Zn_wRS!k2yE)_wU@Z!vKug}tlko>U}(@Oz(6{Ow_S#y(}{{(PHTytf^ds& zdHxZhA+g>`g|W##PDu%+jG9k%K>`HPtSAi9L&gE#^0v$ja{CHB<>)a<)2vmD zGoOn`sdu-J*2%2#@B=#M8i)vOV;y~tToROTV&T8Ifm_@#8&i-3$qglbgcu>JzVBYb zmnT{BzORIpWK%%3?w1L69D~^+&e@Swe3=uz-@Y_Zp2m$dr;#Ko!1-=4_9cZc@kzp3dfVXLG>;gW==GhaR^~e~XhjL#J4WEe0Bv9GPMA;A>1I^28J-z*)Aw zyZKA$pIc4@I5Q5T2P%elQ4?hP)`{Dn$^y zo{t`9I5q!ICvz(~RJOOxHyse9Q@VLP{rb;L;tZ%6F%DD_n6(0qyh;%076a8QTGQS!e+2Nx#Gd|?UzX6J;pcUy2BLz0Ch=llu?{Xw6$i3p}5FM zc;OJ-e=F3IV2?R>-`rD=9F9MUK~*JEuM!@smvR-lGc6nxaq@RTb8ej>v;#_+pB-o= z3)ko@nA^S&=TMkzhx5I;4Qif`pj%|YJsb;LQ<<*(*B99Qm+FAA`JF*G>u;ud{Zn=> z=O%}1XOQ%GP|p@w0r7K0hvcF&J5~ZL#@Y!0_xy7)5ZM?|3zWMAG{S@|@eut7uf34x z3NIYVM)FMr3%lkzo*6j#w%r-B_XA;^L~@}6pHnOOe>ZWcaDz_>)3{H`6sK5O{Rskc zTR`!q+P;5~8p*;x2to#0B!LW*4`QKQ-uA07)_+pRb;v`k?kpuRVWDiEO_vceEj z63&sm`PoIR>rWDXZixPNK^0zT|AQ#6aBE)`AtfMh<0|U0lrr&t$tCS%Qb?TgEAAgw zFqX^F+U5y>h9%d~%U3yIn~@=1-Tal)SpP*oA8V--%jlhQFGIX#mG`82Q`osghXgOxBqVVV|lrEoepZrM597hE^j&ZA2VCRP>9Zaq#u z$WX8whHTEj=m|P{o#%0w++VeGqzcr`Bplili)hy?`J4KoWnvH;y- zLew73+r%}f9E2F=j=zsv7REvcG9|EhXZBl_aGHJY0->E1HLVS&(^0oA%%tRPahTwy z`sEol`O`KwS-w5`rLT(|13gSqM~{myzlQfQfkN`4N>CH4_G6w&nmvQ#;x?Fqtc|`t2u^-> zZ5Cl6iFB9hzT}m1gJs#*x!F0I6oVZYZ}9}FkmhYqaO8Aj9yH$&m5tC^2y{riRR`Gd z@lVl0@Wkg{Jm zTrUHt0W!A$-3CHvv4R6R)Q+K%YpqCHyNhhS%Kf^97I?qoQDgcga>zwfIBss_G*Lk6 zv~Hc-|HYl{P}L6K5>(%T;f6oLu?xdZv4ykbx*ZWRz*A`bw(Pan>jcY@^iJZLXYc4Q4ThsYhHLhFtLTd?L%9k!%3@ zFL429xh&me1M-O`y=_wt@xz=^QZ-;yoBlFBrF$)b-OlO(aAG~0lg%SHM2F)i8z=zR zn)=7zat~$*f#MHIByVI?J{DD=tC>Xu4itq;9odNAFpkr7{9!OyWG2SX7NiYrRQhfT zvF|Nyw8b%yW>*oxXP6A*$OT)Rb?6n6ciq&n;bl1CJL8yTwinISCt(YLD@7`mpV`aG z2a-@lF)x(lUa_YVn~3Ug8!RKo3gL=8N*;kYmlDRu)E;z;jC~Ut{taSvn>XOB+8RCs zip(lBO$!x&gu1$pRdP)?j!TwWtYanl_+YUEBevR{PW=*;d0hr?UL@59^s}mV6Sy%s znIz8T92N|c>sPASWGQ_DzuJbBs{INizGIf_0xAc4?t?@JX(<#lwVcwSdZ~@XcNsaV4cRybTgw@Q4$teVO~V%$)>TA*4IcjF%zAH%E&7 zJ%Q$8--JnhscqZ#W0_?Gv@`uiuP6rG@M(h%YU{I$M=#dhcCUG|9{r^gN)t8sZ`qt5 zTjXLw7DU=U1VnzLZD^FF6+I27_Qrp<0f3UDpCsFEreU$_*MP!Y4d+4In&8jCBk*hE zV};&oM}?$(pOO<^qkRD3?Qx4VHL18w*L!+Cil=X2wK8IS90ftOWDKV-e@7{Y!v|Z) zLkxwqCJ7EDGR^^2TTWSqk+vDdcDm3{GB3vdSeT_aWz#STbYS2$>CovGkcpyp@n4B| zqn{`FuEP_a*;9Famw2Px>Z}=6()V`0I@$D(P{fD@{($8?$n&z@s{UH_{)VV;AN5YY zB~}6zwSCYfQ^|7$ENQU*&ia8o#`8i9n<^atu3OZ8I(jBfuEgzk!W|zk0nZZzBftxz z`xUTMVW0HAvE(y4yXO4;3Sz>grk%m_bas%O$Ie!nrvQd1X$-l$vMI@XF~bbtZX{V)I+xg;92VF7jt zCw-(T5w25pFg=Jc0gI??aL*a9l3EghO6Cd?fZFjA^@{Re<%$!t$eegS;fug9I7t-x z`-TYBOg8`p#E^yljp8l#;7BO2g2MiLSzFkIm~Ci-MfDe|l{>!CNAWd4U;UPDk4Hk` zA}w`;JDhJySl0y?v`|1~g^ECKA54+Oxgca0iW3e9!z-*FKEdK82m`Tr8-N7wvaNq1 zUN7kWx%uo~!9iE4IbWX+k_HVJ^tU@Rz{3)mle<@Kc@RJ;JW^W=fR2UBQiPUR-M)oa z5i=sd@d26DgamOaCzHTxTlt0b$mR#-G6TEqYTBusRQHBrC6e5#OxFVGs;V_xjn6P6 z0riSV$c6{`2bA%>f7N}iNiyE3H(Vtl36-;1@=M*MeW zvhoz&+dAcKQ~mlw)6x)uzP!+DNp{(i*@{V%m!wUi22ia^9uKiy#yz-nO<>mz{|s&{ zJG4IIT^T@VJsSpa>f;DN>nQ;V<$uft&fVp;E%%lVY@j)N^uPKrbX{qRi~gc~uHM;0 zlCd`g@8lRq@0))^XN$EqqI$Eg&gjfO?f!#K5)|sLunoX_!VlJ(^D&$RK4PMt|JDDq zeijL1?MZ#2GcNDu4MI0%&IJcjjF+Z9!;TQuV3%Rn&XSM}cJUO`o>Eagu8r{4>kmTi5*h!42L4^$A3|_%NE+cTp9OmpCp=g-eCnnnm*E z?b$~Gg(E!3N6Q1QwGra>g_cX(DZE_Tn&V&1L7?)lQqC2_k8{@MUuShH(7JYpU z7|zhjg`w8Dx>1(+ocXTP6sOn=kr)QmF$ILJ!MF+SB?&5R+i=fJ&C&d&P#ETkSev!c z_qLGwu5ofN-r5#kKtzKCm^hp>d*AWn(g_cwrgV`UC&@5j2LH@?eidsc+P9{B=i>p- z*FBe$G5GDxO*5BaZPJ1pWa%4Cl1BmH+9ose^W-B!VQU>2H`r@>%GMIqeulM+y%*^h zm*#^}uhMc2T>p;-S-F&cN9P#jQT0%OKE>vD7s6ik6p?Bw(*vlZ`BY(jJ`Cynj(#OJApN z?R#eT+L^!{>lGCYS&hag%~;KSG~nF@U9XbKyon=RI$b)f^^~ud`JS-wDsMG1I5=h| z?q^0U*`f(372NL$Ayv>ov+TmUcPb`ro^^SvMCG(H$}Vr9a#5zXY1_lKK)$mwegR;# z5&jo8F#z$E-&nyxsxovyp#`hsA@u9UuAW8@h;{_H;?|`Ev}0ZcKxEi=2N3CD-`s?z25-n=iU~D zi1n;rJD)GB%G?Z3P{(BUq^tNN7Ipr?5_i;|b63N)wt-e|YCSTwSK$L(S?e2$bgpKO zFuxawB;nUOWbG&o!`Kyr$+6;Scf~iN?I@r!fn~~SLCCdl=Hoba}j*XBjyW{}zxC#dx zsbgLpO+ds8I7?&zAr_pbAe-1^8Fh;I5QCB|QS}1K6>`j=v^pkI-BFNZF%SdFZp^D* zWuL^Oog@)pjuPU6l$ScLTjqG$V6em-rSKC9hE>k5aOViUwAj54ZJbZky2iN?bjwvo zj)T`crOJ#^!{dymS;pN=>Kgi0kth_7R=s9r3fmq}3~0AS^Ir(rf|>@~)y8`)z&+1Q z#rddQgk?{amKHl%)|2Y7Of(Q&RYSb41)!7weWQgm;8{%Hz2Kic%=v*|(Mpf5PBupp#@;z`;r zcCM+)!<7^w_yA6hd`Hhq#+*7;8#IxTZb4guOf3dN1l3$dU$%poG^6us37l)=w2eEb?9d{^=^yruJRIT31>VOMHFEFcwn$PFKThgJR zBwWs;qw3R{OlZDQRGEd&_yDMz%+-=Zc>@uY%*a=oMp-fp;X{dv(nUmoW`;X@@Z`!l zcnup)%}7~`Tn_tbEB2N$fZCoAfMpa3ROZwAkR9}8RheZfZ?oDT%NeVeOQyb0Ft6b; z^w41}OvdO7J4YsGT36(ndAbHvZUWwNLrWM5ZQl*{L%+@2RbqWhE&;j{>c z6@3x6sLVZ63@h!_)gx``70|wQSu`kin=E~od>puH(m+2n@qrt`grLGFZ2}Aig*KDs zK`+DxD^CUjK(1W^!EKIop)fid=X&9M%nUJdw~%F!fg*{?b1-aPeSij%+5j}f<67ua z{yM=xP^}xA*GhmOk4-+)ws7q9pA~V-tSGKWn&kE;k{v>IMmi7|6VOhx)VAeBA_Dbp z^f;b2I!qALgNsW+vn70x+gjqvZasWjj>cYPJ7B$qtvd!4mFm)-J?Uaul1yEyfYt>d zA$|JqaN-Qut}T7kSR!8R19*qta6NRO&7bv;NxI(?+s z38UYB_j@@rBoiP?I|bh2*R&q)G)3&139JmS2gVwf-TxXzlvE#_IEzt%k$tos=^VNT z#y{&@afgns{&M_Rn6Mezd&9+ifV2c~3f}FfI@=)$V~#rI0XkJ5#996T%c(FwBSbYjwrXV~M1uJZ6L0Be+xv~S~XWA3`r5q%lP`yw77=~<(+|+iRB2TvKv2JX%z!#RIhOvz6k8qVZPVbLBPXRUL`A* z$tDI0oI+SMexg_57)hkloip6=zmpP zzF-GWw8&&V-SIdMW!@LHYPd+O?mQ+)+{JJ3{-6Vx0vyXeE+o&?RSncwqXIl(Rm)rC z1k5ZCIpF#8^Ahb&lTl4jAp!)@D7XNjbX^#*Da&lCpv_HDtgWFKO1uvjP15C&D(J(* zZY3VtrOZZ(G^I23(sCo4FsD_C3(E%2WO;&?zkxmwWbs`rq{m$+ASO0SPSeExiQM1Z zo#@f))Mns>;uO_z{`Pt55acblQ&Ko{L|He3eS66qY z|Bk1Agf0e{7*7*qT-&9LM*24rA_9VW<97@w*KxdmKS893-u*j5(Au=iKdl!Cl;mL7 zQfmemY!0yG{)W_$B%A3O7MV_iI2lShd4j-_GM}wH z#;vsaxJv{Iwx;#S0uU#0TGjhT2mBRMAhqcskG&CSnM+*SZ?3)b-tQm;qNFMt~-C_>n6_z``N7^Lup z+t8(RUZE8!!OaZTQKXz`IMIgzlFZNXKaWB1YIiAc-b3$4|I%~oqLUiGFMC^4@QpZW z4d(#~)2f-u7(UCg@h=Exo&jHoSs77}w^F-L*VPK+J)A=W?2uDsoK9>rR_u-&BtH|z zDPi=Ve=|s~mvEMGdAWDw!YPe0e_Tl+k13)F3FA3DzUBA=f-yPcAgm&Tf8iEc6m0Z5 zfa?hOwsG-Bj65@h^}*2wg{D?kv6~nbm$kASUqwk^GnjdfH~O*mj(HXs7igd^vpYLg zIypUA#&i6+0Uebf2!pnU`|e*#Epg?j2%7fL&(?t(pb|_)YRnN3^`o&s8)(LJJ%h>m zX-``N@^4SL^7<=Wq))6m31w&G)*rx4CwYqLD7%SWEJqZ)j%%@(VS(Y?>}jv*zIf9U zD7c+sz4)QIkXCaVNKE`ibgfI0T1K1c^#IK?MF12UMWyBL@GXue14=`>ETl%3reg2l zQtTP7Rzd?7KAc9_j+)Xq9;9^jM{W7UurU%l6w+{uzaZMa;0-JW2cXmG`jcql(UFr; z6^G@Ah873J0~)E-!rdCwk1)+;abpQTY5Sc$h=DK~5AfP1GnU1dl*=_@)Nt1T6BgF% z;z&pf2H#g)haGL~93jzfe)JLW(*0@+{U&@Jy!xaFqB(eC*>RcKrsMI(N0ZK2O6D9u z%!Ea^SO6sybia}q&|n2%f=n>^`UXZ5Iw^Z40m#6harjtu4_D?>9N1h zwCRx2izPnWnR*#Lf1%+^9h(%a%$;S-+PsZO4`svW73n-+1^ruAa#5JJ_Vq}>sm2~R znCLZh;L#b=wgdP0kO_;CJ$5dsql=UCp1xQhk&ya1N>D@t^1g155lc{dr+;0%Bf!lP zVw(+#RjosUiwp&HY*xDM=1X^ix;F z{ejJ#AP>0I>ZbHvJ7)MGrASN(LHjF!+JZZY>MFd1qJwhro}~23B(dI;k|7v`yuKOaHdBs zO5B^UTP89#yrlvtJbZdIs~yX;U(Hh#f^t9_cn}eY`J)pGPWk+Og?6|OB8yM*qpO_u z9fDYZoKC5s=`Td924_Kv^z81W4?dOIyro~V{oDnQFtGp0xB8^#7b=ToMXsF4;tbZ_ zOVxBCYVj{UaWVgQhOiZwpv6+r%!SFId6xz^+Q)Hjs+Q2M zO)zBjgK6wIt&GA&Hn-y3{6-nHphxn{nba>}1Dpd(8d{pRii+Lv%WXWnV44_xP#1F# zFxlb?%f}&WEksN1VyO|)&yfMS_r?Odic3r+EN91wO^?IlQ6k6C;pQA8y`d6biFCE~ zhq)1UMNWO_$QBioFDMLa=cA0sMqjLDo!z}@{jz+M{;P#){N8w%@_%IFA(;E zL+ciJX=pm6`WWQhw0woYA1<42mqAD{E3GQ&@Kw^A4S=+!gOs-DoF_k}TYZ_RY9|AM z{f_j&|DXVrUEmr`Jsa5VA<_Cm*yohC=B3OF9eEZFRjgp&5wLC)w4f2X@w^II_sy9D z0se$&RptWAS7J&01Z6vL;GM&eOF4At+wuQq`j-qIa1S5&<%)JUZd(NQWc^o573Rh! z&+hezHepPFJsT88dqdk%(R*M(=Y$4wt$+miDqq?z<738jLdpsa1zcVw+3&N4HW*T( z-s77JiaX1)lCYm?Mk6R4Y87rg+VaP%sgTNb%-n(BVi^}sQN}@!NkdDlAg|Z2ckTWM zG`=`xFik_~+vuVwEydz=Bm8I^){E;6ru(<=1_boB zo@BFWKY&X+6vOxtQ}6^aYfn8v=7lD(B~QC@ zh@aUsB%Yqir{XH$%jU!#Ww8@t-fSqR@tX$6Cbg)HwL?1ZoePpJggmY<{a_$oQPGFxvDeVuTKaKGD^CSuT?;$=6b5BJ9EKZY53drf7|an!UlX26srywJ-!^4*1s1R z?LJ=pFGO7AC_lP9X71=eYKZEPfZRFb+mWXQwtR|KtUSG zYor(!t+9i6-!EfL0i#Z7o^`cuID$Hn_*||HLFs=pg_*w_w|ZKM^e76?_59M65zl4Q zP$KZ!V*b#r8_1AWOp4=JK~LjT#1_S*$E!?q5uWd&e=mrVw^su5+Q?6qNihbv%pGgf z(tQYmkA58coEAO80hT0h2tv6xKin$1ZSwdLwgq@W#(od=F^d$?(S7|qSpjC0vaInq<(q)Uhrfv=5dyK~Ubr^ofuw~y8VOpQa0r@uVw|>J( zmrSb>ITd!no4>7o>pO`R!>wipOP=|Zi47z}Mk>&_e#dRMf?q0%g zLw8~U#xWvl_M&0 z5DNP3lDt^&YZCyH4PTR8Ou03Ycy%BEbTa=Nw4I+~`xgdraAks7d*IWy*Jit+kUtIu z{hE#Ycz_L^AW*0RR;w|8-Mle!%vhu6;AY%A>-B9_9SX0X3<$_gmE_6N62?6|HN}y@pNSGJ@1|3HvDJx%HxG?_2GVL|b4*#)OW^x32 zH-r0#PU;OW^P-#uZ{_p6MdDVC0nkd@-QgJ`}D>9_1{QP!F;CXk2VM0=rc+s0ABt)Ddr3= zavUlX#%z*JUmTU!SAsN(SXJc(ut-tkkjk2KCQ*)c$hs8cbcl{*{si=%sir{+t;t@y zL9?PSmb#bMO~kUJHL1+OW3qS^O&f33of4*jS;7`-e;H9Kf{gK!*e2(Mz>DxG!va4D zMEr}nx}dRsGhhKVef1OXltY|R0f;|(`8x`bi&I81z6KeINBSrBqy8&QWY5{?m*bm9;}yW}y*E4gsQXv-48%~H`1ULGxy_h}ko4-_>R zE{J6vLiS4HZbAVsdwOtw#gPSz9j09`FrI{JB7Ti>5h?jD;!!r~4W3tc3=iKCOzizc z5C>BK7bv3I`PRKYN4W?N49F;q0b1eQe*}1ti^K|kjRxjkAgxMbmII&Rz z8+jS^uJ{CaV&iS4dy<26jZy=X&DkNpyK-S&7aKX@Kyy2}@HJS%0a0+s%#SYJX4DD& zUk4Z%tRH+XLHsIO1!rF=;Pe13{Rb)~U>Umta54a`)VrPlPdHFiXCLtA|Cc8Q9h4a* zxxjf}mKnoBEG$&0nid_>UEbTBTU*!!#PQED8{Y#y6iVkP$fMCq_(9cbwcHHIgNKUt z(-^3SWF327b|ZW)hF$0#2kupc6zC*-m|#u!qWt9kH=I0*^a02nO34v1ehoyL-e#2Y zjQ65-RW|&+g+Lsa&SO5OC}*$WXn|H~x|Pym=vASECSe+~GHS|{7L9^? zMs*t?i580A4Q7)gd`tf#J5vqu#`mkSIYh(GYy3s&SRt3PR~bfmP3(i?&~Zakk+j!& zL8;^9t=%Oz-mNZjz!?ijJ<<9(g&X!%JH(~q48_7R#yP%MYq@x2&=t*fhol5dcOlfa z1#AVg`tDzjDcleaJ?n+0d!LviQtj`?OdDnEl|((ujH+-hirNoHx7m8A7^)@6I!pz4 zMn)bSDZK)!ZCz^5Bd&bFTU}jYDO<4OC#Q!U)p*GE_#5nyeDR%bT09E|>$KLV_Dhx$>!<~GFz7nZP22Doxzh@VEVY6lzh^PpX-{bCZ= z(#;r!sy|(E!#V^lZwmDGSt01Iy4fx0F&h$! z3o9n%tGF4$T9einU+up>$L~0Zw3*B))Gt4myUCC@s&zG4Cw{qSQHtNxHyDLBh$Ui7Vmg=mV^^iN== zu-i|2z9>ok$o|VlD~UCY{Cv+3L>&;%RpdK=8mO@~yrDAytd%xzvXhhe#n*TiSR8?| zG~c0jZA&T)4sO?d7W{`*%efb-3I7^Ov+pkyad?4`g*}7-lDEl6&@y1G`@*@V03vkm zHIEHC));v+kNf1+qacrAX*{%0> z`6sx45Ll@;^7$Ejc;e$cLY?ii{(PNs_Y79(6e8y0KZLK?HPqOY1@~)Fs_Yh2vH~N zJiG~jva!OlV$bO&$|x~}DR+&Ar%2R!5neV7L;&B>ZIJ~+BD?=9}t(nmQ0T*Oe zo_`j&s4GuS$c*INhl()u!=Ny)I4}X~4yV+Ki;k~p*{)fms5%fnTF_z1FjiDb%G2pj z|6UFHr0UXUu^i>W_%bYp$oH3Lu@l1NyI+t;Z!cfD5rqP_yMxS=OiiAWd1hNzbY!o8L@3A8ggGvf&9u<9~!Cl7uFA^W%F<3}e>_|w* zWj8f+e%!YD)l4z9@IGM+T)HIK_F_lp zzPPUAj0{F^VwoIhc}lP3PC^;HTw*ZJOLqOC1|nxOwY?ibWVRcEGZXWiQk^*G#mZuO z*XXX?a*Hs_V;o+WI(re?BX>qK81Z|E-;BB1EF(>+TB&4FIIZaU zOth@T8pks$mb(do9fa`)kzP9taiUmn#CYC;{w6(&=j(dUNyip$wA-*-)9^%f%)c;r z0?UCNT)G%o;;7BW8+RMOeKwnwc@ly}v(e0?)4WC`C)R_=+X0pO1j~H}XEBs0(Emfe zMGpy>n+*CkT$*2R$iL9Gy~u>nLzX z8?GuYkUTXLG_Wo|xW+tpemEfw3CK)D9JzYO<~OAET_OuOJ@|}3rVjwJ*yu#TT2)ii zDp{?~G09t*UsfO4P#yj_K)P~Bitrez61Lm8yP?Hwuq=M~zt{-I**KE$3Se3v4u-bf&8t!1?`)>LzY4` z&(L<4)O?bcnt@FJ9cVTQzA)>!wmyT5#kCMOf^3f4Zo@?oIvWI&Ao4_Y zj-9luO$kw_4j|3-`)CKkZ4kLK#+hCwyU99LDSGu3VQvQSY)XJ~_P4h2EYQ)DgwJb~ z!^OIQ4)ROJ{agv{Fl))TmY>+fsifu&?5(yL*lkQbrVU}?u4BqZ2C_&s(`tVRkhd1C z&BWOK$|nX~n4MLaYj}b1>6Tx|KuTYzC71g&PKZDQ-fRuq^Irkp%D!M;?yV$3Jsef# zad8ro+Tnrz@D-b#N8S8tXSL3aIeRyG+L3Ewjcn^fJWMqarLtUESf3yz330O za*~D$k2`bbQRiIumv+n9Oy*I3Nt zPX6fn`;iy&9!v^?@wMX~no6xk%3+EfzQHB;WPo`q$7Ww%!mTD&Ld6;X?*E5K{Kax> zILeKc7Z^`F_)x&mKlr`zIa9JBD*cSG zlI@Kq+}pcH+|@#qA4@8n0QJ$N6y5z!y*-kF6++G?m)s0_dn5iC2|llsG7kCD+{xV}YluP8!s?Ca(!kbW2 zfH`u>HWNfc&j28Ya1jcA6%fD#97z!&|A_RWNPNH=b5agj=+4znaxx%lc4gy+zoXgf z+@C1}#e~N80&{;ZwUM3#bPplo6Rjg6m*Uk!++?W{J2+?>(~J^k616s&bTBq(6oB#R zq|Dk;`v8PM;0kjLDOm33jJ(aa+R0h439_#!uqd*u=t3EE01d`E*`|IKJU>e>ZAnxI zuR-BBe`j$O0SJ9w-GwxQo9%wFW%^LRdPSz>sr4ZSk>75Wtk9LjmZgH=cb3v9?;Fi`o0Fqk+#>V5jtSuzA`aq@h#pkdl)|S!< z?ki<4Hgf&iaWYXdeFD_ex7~a1h+*gOR{G{)D>o0>TsPYxqmY}?Gu9mED@CahbkQs8 znKcZAl~iN`XZIX~d=_v$$;R`(V*1QgY#x4gfZ>Y<;o!_13zYqU*2QiM7-`_~XZBwj0Z;`gK>^dpWb z==uGsLxh`Seaj}fu{q&qH*2K7d6uHuj0O4UDLsgU9j9mr?M0*kiHXi_xrwbi6bvN( zrPNsKJT#O^d!%OZCqutPug$TUi_CFAa@s7oao7n6E!i$bi1oHj=uVP?rO+E=FPnYf zjnKH~)0e`9Yx#zlQ&1z+^uUtC3{Ruj&a4Xxj=BzyBug$08Pm$h(?vE|9ia!|(X7q0 zmGLRTli{P!$EEYzK1rUP87V7;-|X`aKa9^8iM2aGZgrBS6T|QyUrJD?0uKy{6tBP~ z*|u7W&@Rr2&GetqL=0jk!$tWBbZRUTG`Jm(<8$rYL#^TS;D%uZeCj8{1<{wa@P)mC zz@OOSP9{JS|K^Fmd_{3=xw^VG0xKL3lW{?o`5vYPPtnje9+({j6KJobO`eL>8iUif z5GYO3ikfzNJ^$h%3xxNM;Be*_8RT%W z97#b?i^}e!M2H+$|B%{hcwug>No*5e&s;o8dQ@t7Vhnh%1;mAPQ1Olp<1sMWH@N3L zXKzbWxp|QZ7n=Q5q%oFkouiFZz$Dm{@@r4%w+|W*3>JWQhMb55(>Jqd(?192&M)3NN)Uppr9pB^d}q)>s=EA(H(4^a#*2m9`ZH5n{a7=O-8EY^;ST z0)$)Vqb%=Z!uP1~+#jL=EjZ-4)@p$l!8~yBk~lT_Vb4mIm<@f`jte(-aaZTc*lajY zfQ%CLX48p~@Sr1#?XCXG03qB1b2+Li&x{t`yLezCQq482H#K+fh}K|$b-D2OiQzxh zZCy@?O+7wWV=X7M=L?z+@*{Uv36otGh+FqX5vMTBJAa#4?6-Gf);lNsw2cVLgTs=7 z{4ZoO_0L*l^c#W>N4_WeGEeS${kV%$CTS3S>!Lh1>UBg7>pGAjaI0s}j!l>!{G<`# z^&jKS0Ve4KMrpTw!}^NR68+Ovc>C*f8HV=U0`f=(>Ae@2s;JpEgDeNyVD!kr> z@wMX)CZn00Y?EhzZyfk?%#iVLUzwi4aHpdN=f(zuak)AS8ZMZZyG32bWT-zk_XrOY zsL&^&az)p|Hc~CD*ibu;@A(Sq=m#?&4{62>a91JkxCHC(#`NDgl1wxtN%RjF>DWN% zEsEr~VBBMjovZxg`}m9RF5l~WzRY*gQ4--HMSZl`{qFxq<`msvq4*nWsxKW$WoF98rPkRt;0r9LUvCF2o-VK-EDGk;+cVv zpM?&mEa`MjnhKTp*Hb{5z%ZtcB`3_MIXf66u`|;S_$|~8PnfPM_3pA%`MiF8K>(?m zY&l5Fs~olV#=_rm^7G9ge!NS~*iy{Rz$)Zgm1`jf@_4{6EM*S2>6nk5i4S&vs6$Y* z9Up#paX1eEG3@lsOb&}ULK_hS~?KYtUveyV_G2PAxpzVJZYm`A#&;3wlsV-LgW;ZJLngR>%-6wJuU6#|rY~-TS`)e}^b)?^z z?|gNwcc+6Zs$exr!okz+-h24Ut~p2{q6mW#wwMGQePz!RUoN*nhV6g@M>>EV{G-Qg zfd@7NqZ;|r^GV0M+;q(hj(<(t+4m!StUKLvGj|NQba`KAEQF{4qcu%%uH@E~zOwJ? zK=EC2V|E`}p0O>LP=9kUDLCad z0)eTCDmHtU8!8m?7_am0c0(1_G>%VtZI#E-&-|HJVI z@`r6e@-7rX;~-6bkWS5>O0Tw~`S|z-AFkouWlG|{-G0ihGltP>AG*k6fdQp!c=9}U zotq(q=2vjJ(4__`AMo$ftreva&|(OqDCxlT0B<$cn3{prq__3)8`#?fl6X%8TADbf za;E=fOn_4_rL>zi^HyOH2b}S01Mi9Md=v!YWV##;cLlf9&w&kn3E~aPlSY3lM7{=^ zuO1H()1*EYHlzlWHOXbfs49;GH||FyBn^ebBKC^HTmilcl*yWyx9awW4O?@6>6e>F z+{N(|29t5G^f)r$1bcTGPX5wxvH<^sYZOVrOtDt~0yxJ60 z(40&{H}^aBuRCl~{fs7cpgy~m-=!ImNU=Uf2%!=`TgvChr05A4Rg~km+8aqzAE(qS z{xR-j_^m!0cT5r`b)pE`8SrA=RX4CcbO}nX)mAwKJQ9KvX;cT*eHNZ^$$gwwH7~{2 z*GGp#J+97iuJ6}=UGu0b^T;cw((!!&&bToJn62;v|K_9|L`Jkj{$?mr;&bv@2$4M< zX*93wI?(61zFgY~(0g;~r!8q~Bv)#3&Sg*)K?H3tI|r4n`&ztQWQWE+0t7+aGc8x% z#A@KXZpimtepog3S;sAk+AD)_X)A~p+{;#T6Gn$xT98(Jz5R{Md8HqChF8OX=oz{J z??of8MKQDN{o2IIczZ6M8Xm$I1a7;qDhqFOd5d}vve3Fo#hsc$-D#*MBf-fSmLQ!}D&BgZ_%D zOkDa=h}=aNeEApmrO}n@JtqZVLR{IPsUK1Wg#j#$S=Z}|?lr&RX}caGq0wKitL@6_NLiVR%*!$SVRp(K#Ad zAt_0ytip)rW9>O(J79%K>{Hh%WQb%ZTgxT-Ye2FJ7;Qt#;{@gXgs;|=>%p$bI3VBS z_-}9)rSbpAx>xK9DUc82n|mG0>$4mF(e1Vww?~Zi?V&{rY`QYC5Fnp)b0f9-FOKD@ z_m&}3+3S|7Ob0j5`FbjwKjOr;O4Ehp}GV_$MtFl~#}Drzy;SdDv5D_8|R+{)-2oF;`?H6(+!h zTrZYUBn9v)VOEC*esJHQo*CiT0xPfUtf`7*3qLT=OgD%RdogUSuexUnznqXXW|cDM zXMsQ{I(c;xFq=|o1^hPCVPRjaUr08}r-UVhUTV4)iT{SRo_DxTdeB8j4q87Zh?yeE zmA6&{GzhKX@sHhvHFy!;3HVqp^ULF{wC-*(U9lOcW$_W%f_JkOaMx#o}v6@EsTIn>gh|Dmbg zOFD%FHI$hvyU^A`gHl^pBJi_~pT2Pc6o;MIrjEAfe@UbYi0y^Z{Vh^X8Lx2ZC%Eu- zThX1z62cWHLP<_~&@Yh0I@6*O!mGY1act@G5ZH}=4A=x`aL=gK2b*G}APHWJ zj5fk>{PZezNkttt_GU~)`CdjAGY$9Mql5vU>Dqcl(%DVi^1E0D8#FvxZdaC?E`*NQQ-`~L{nWBdq*4(J=w={PEa z@Ujw@qcPQV=lZtsnxGr}1 z%x`Firl;x^6S5{4@v*(sKt77I~C2gN`X1Z`8>C6 zj${s z*8gb62_QJ$R1cP7-0xx}s4++sA{VfVO(VV+IIh^7YDJ;quTe`0#JM7gKX-};W)5IT zV^tXa+b{MDUxb-9_E<%?aDL-s#4{sB`{E|JxdGp^Fj57T(WeCwkxT6qVcX1JB-4PA zd<{+UN#9YMwL14bJ`1va<8<;^sQITB2~}J)>Z|XGcnc4VV9o#uVf^FlS;?*D+6k2U zW`RF*+`bY50fRF<_=IPjUP0=e`m8k>hp&=n@q=fxqd&9*+7&m}zAXc+7Umho)17n$ z#Wj{`3eKGNQVc00Tf3r;c!G%8I_lduvw7R^_uq zo^${LuIov81PjRbh9th#Hw|R&!Tf2W0t$ zdQ0>wyZd#ALL!djydAAcGBRcmET-nH|Am3utcoJgEuv?Jzdgcde+i}&Of5M!Lh5oElqwDs;?_RR8;aDcd`JYle> z{;QD=0aFR(eZ*%EB-WVO;x51M)r}yhLI+g&=_IXJvNQu0S2=Mp0fKdR%8s4)f}X&3 zAjQTXy(oYXZ!S|2b&_m7CuLPDX5@IQ#LEwXbrGhdky)XV$LqHLw=EJIfP$=f&p2nr z{vli!VzWwV6Jcv3y3Jl->pB~2;FgjDq?{=c1Jsp*mZk{TuADTuQBACxoSB0X}9aF2KTWn$`E+vTiSlTM6_-K!oB0kyDWM zY>T;}C#{#9NQ1dj{NV;`oZN@nVM{pivrSX-C!mDPBfV=!7W;5Wh=nW=kk)h*^2eCE z4B?c-+`6A{CVD7X=w&vf*B`I|$i}ve!PV9p7^O(%8duu@jfHdu^T|Z8AJ3(F1<4s1 zR>)MFuZ}T%Luy|v1ZB}ql@QVfRrlY8uL7j)KbDIP4dbb zB7=!I4c6-v^dj$oE*@$gb^hQM?~alJ9tf0e?23~+GYLW4QoYG>#&}}yAm-UH4c=gG zAkgObsIBqnez%~fUmlIkg%hj{EU-XEehD_Z#mQ{c0pOe(dY#+=GhRZM!4uWoX;2wF z++O(MqH#GJp4HWVHlubE0?s&8ax&1U;TWsFub*5v^7(@+Uh&M9&L~tkafXgW?7K-< zvd>~C6edCf=?;noGOdgC0aD>4xWx+!&@}jUn+Ku3vFL0sljf}?nFJ&6yF~$R)H@?S zD&~y~^_XQ32C|SM>j)g_9%xmkt;dFh!D?>%({G)=3SXWF66)HjY*jIb09kSr02j~2 zmn%^dH>a-;tNoV@tF}RHZ-ZnNDnukLX^8QU3^Cmn(0_FmDL`v%WEJTd_Dkg+=aI)5 zyJzFx&HJKva}V14pD`9-G8v3dcnM{xowtvoKw08C0e^A5d%P-9e9yYDpUQ<4E1XURP8OFGVxTaC{8@Je z?hej3MgVNbZ4}2l)Wm*Kr`)PU+M8;$Jf|SS&oSK%n3)Rz=G<}9*Rtgn6KVrEv6naP`?}xW64qtz*JJqgU%{u$RUrpkbFTqA9>pFR%jL}kK12YJovXK z8CLohK>~R9T&{!gUW3j(`{ZrI=Xk7MJe57Iw+Zx($9JCV$@uV^Mk4O+f1=0CsMI$~d4e;Wr5pa0@^Nk!jxT~R z&B2Pan?W=3NWgF3-Rry-!VK&1R#a`XgXqwf4{ur*uEuu;wzFPDe(ZJwVztOEiJ>EX zm@e!=%|JR>7ostiBsxrI4vS)RJ?dvqSr;6%!MeA@_u8!+C67@UV{`{&m*cJQZ79Bq ziH{-B5_?#9!mmE6@|T3p-jL-zmb$xpOT#}Npk9C?b7fWnU`b_2px*OsT5+t?>>Z7p zI zl2XEV55m8^^(OS7enb=*Er?vva>zN17qjD&KFLKY4|^;JArA6bF2fM ziEi!{G7^!$k36hjsIogCGIyH-Pg_c$7U~-U1|6JxK6KCqkXairkVr4|L@)-><3(8u zb$D{|Td-&M+2DGfkT47e-Aq!aX9k>1LFyY@I8ZKo;X&bZS%`}O&%LQmCb#$gkYbyv zQk%-ZH1@l?vu|t>B$W(@>Qnpf5faJOQ`)~*QS9Fx{*@)#0Hkr$N?V;M6D*OgCcs}1 zN8lLXD2J^X+6$5mjx)o0GW3m!6;&(PJKWH*T+6$n1c+U(t}>G{bZjL9G`a-_`HLpzzkso9#nsU*>vu2?ZPx-^n zbg5`DYzsS7?{u=|IAroo0ZhzOV?%?-oamk;x=Li0ku@OYK^r9X5)!Bn9$A&WoHx|? zKG(^*oSFswr)WIU8rB$#J>q8%yh!Yoq4FFF8y@Q4L5nbw|s{yhjd4uBg7bO|fZ^oRkAdp6Y0g9!?7=Jvor3NU9$U&5d-+8_-@rjfRymW8 z`q$BB=2}*IexXuw!YGx}_eHs-iOoa`a*+D-c^U@LhtsR5Udoh)G>H~`C81}SWZYyh z%w(YFDYlv)!5I82Bbv4_eFD)3Ns{UfODMKpauEA5hl%~RN=?5vn-n^Ez?);BKTCmMzR*23F@0|w@OCqd@WpyPzlteyHlF_*R_es!N)`h=w}WDl%-BMdbx z*a{R3hR@tcN|8li2!BD_J0m9oxDs)6M^*8wd34o|7JOrjZ)VN)Z89b}SS}}Af{$4f zAPVk2oLgXbQJ1wcuOBbl?h^-&ExP}AxMSq79@ zq~2|8RmI~yMgK5XA$R9}Qej733Waf?$ zgYsK9sD!TeGE0Usm^-TEE?NsC8Udo;51{T<6<&efku?nrx|r$MiCHHqhYMvQr3Ccl zsLI`we3Ot)om$+4JqHD)yPM!lt*lECoKom7w>u#N_9zsUk~P1XjwZ0YpE?OP@U&$V z2m}~y58zLt(EjgnWfcQx9J!lbP9RJV#xw2+&j}(5p`|kr4@08Z#87I)x(kp47!RN& zD?Zu3q^;Nr3iraMkgbcci#v(bW0iFZjxpK}y%2Jy{O_B;XmW_sm~Yf1ilxa2ms2Wr z(p!1pIKCT)A*OEB<&1*I{8!&c zg{yAkA1<9ed$~TOwq&LfUoMt&Pw&{}&%T~pa%tD0AZ;v{;jNGhKK=d)=bQmnknXe3 zHE`WT{!`}3aNchVyq*h({|f)?&vTl1bUen%9iPeMFJ(!S^KuAOaX7dCu(&zE4c)!W z$HW-t5e;k$MfqF-A}PTVzjThQckw{yw+$BF{?G(JopU^zZs1t4A#|xz99dW_ACd7N z)-UH4S1?C`no@^6&ur2l6vmr|ifK(5Ne4V3!X*i-RZg5~MD9CkaJC65)~e!B(w%q} zp{9|J`1^A|MlqhywlkoPUI{+Nw0+l!LSHdh4AGNPGajP8P~@&(0h^{w2^!xJ12~C- zp2>U1CNIk<)8e+o5~bz{@u(*alwqiQZwF!hmRWG2<{5kqT}ROeeA)XEnZhSVF@LqJ z96NR|SN{D`%$LIdHJxE;_PH)$r?PFz>1C+MlboU+!+!DEf&?WNEH_q`JJ-y6y>E9> z8AUE)V`tzf+9rtlNzLqKV0;|1Kq6}7d~P86!|8n`3IG2SVWmtGuk!R{bk!>0LW~HH z^bR1mZ*cip`|h7`^%p-v?D}MJVAK=120tkt6{}$oUdx>}l^7%?#=xuwvz&SR$ zHXzM+Zt<}|1Zrx_*Ran4D+lG}Sab|klZYV?S(07J{m9+-qtujSTW!sCcR2Gy%S>bS zh66Z{M5qnr+2QYp&UVd|72GBwAlj7`OWZObV=Y@-kAaQV@k+k4QaB9aIUD7u`fGX~ z%k%GR5{^#BF?jbnJ-(&~$7O;7b&W%y9IPp@2td7GY*YWpouOEfzi^$>6EV~#xqP8> z!X9C`QL1ao$V6CVoG>C4?n)~YNk~+?T^ykPohsYbh`&+LdJ>zF7AS$Qrb2XFDXL_! z`;xDABs^|YPEAe+wOhwyu`vwYCy>D zt(_&7BnKS`<7AxvRaONN@^o!M|E$lBSi0-Helr0{-dHwgqazuYd(v`k`UVhyPFr`8 zysigxy(fzl0KvtbE&0#eG5(>MFs- z9=TZUF$8*BL0*Q3Ag~**^18M&`3V<;=lAf&QVYru2_kj0J{f3Q;}vxR6ZJw2#~oFg zBQ67|@Ak+clD)Mb$yNycDE<(=d5WBJsPhJn!!_gWrr+l147O4U6K9yuYaqfO0)Q@e zhC3%C;a8zxc$0WjvIC}+=f@ClOfLeFalH15AXr=!%uhrJpcs=zx6SiGoddjWR#rfD zpSPxbM)oKx5i|gm))6|H9LhOyI$O$))HN3a!S&(^UMCtaQLE648A%E~6YpHIspW9% zce<$)+j+m&xw)K1M588-&Cx#Tk6`2(U0=2bF5I*b1~^k99s7ZO37jY;r^a}NTW-2N z6VxHi#53F1IasCanpAJRD&aXcr9>bL>#5+MBo-^DAZ`IM6x*{fMm2_sQ4V@U_Qzn3 z!KRrVM{n%}=jF)KVDqOHc%1kMN>+6yD<&xHi!$9Pqs?0A446%n z!>UYSDdt6nO1ub~a6%9ig&0mL4;czbKZfAYJX82EmzVRpehf@~9RpH8NJ_iL+U$b| zB6r^A8#DIC_GnxTAzCB>?IluUblPLP&@2rbLHq&xUsgR#7vJY9vI%F6^_NU#VSufL zU&Hh=$Cq{?RK6npA=wBDnr6ZfY92!I>l7W@xkLCRB;*T6#j}5y&qKI;w$X6xFhOigN5A8w>5mYhDP5VTTbccg3uKT@yNOVl3cx51Gj-Q-&|!5)s1_q$zSY z8$XWwSkr_SGZqSJ@=LCBj1{dhc9|``+p}5~2Cj|`>eaOz?N&B(ic19)3$J8&1sWqh z1y13!$zUkbltpHifYyB!x1IgWoiuy{C+q6=2T)r3n1y38 z`*LO%Mbzp%CWgyl{CKKH>FK&m;fZIH3OJArPgf}FVeQAJuL&mPLZSUMz9spLBoF1y zYSdDF%rNLOt|Bj$3=SJto`k zq8Y;83n@L5{kO%hm4w5f2M9?EoF5s@8QjeflM*=mfeL@#P6ak}(yR{gEU-`~oL+^= zQf4y*-dsp=d&0h*Z8=c`SGP1%-Oa%kr8S$znQmN;^Q=fik{Tq_CIhXRPLC2)L5sm& zyej9j)lk+Qc{UId1%cQ;0j8)VOvnm`{lCYik>YW6PhR#&5?#Q4;1YMZSRov-^nTJ^ z)fEZQ=?~ouHKry@#(!MN(DuOz7!G&AW$pJX({Ok#fo{_kg#Vu6+SwyFKJkJc!PNN% zyB*j8sd!E`KlU*i4pzv}TWArPt+sBh+teA&zH@u;|9JBX2Cxa=AuWjgyJ%Gah?(sP zNo26vl>#pfHzyyHp?CuIHlM7tr+x*mSi(pFPqb=7c}c<^;86D;(i5ZY@L4qs*#t7q zxIB4@6==Fk)_9I7KLRNH!;$eS!#=5~Py%6a0CUS`(+9b&k_jwPBr2r|@DhMjFs*Cr z6QVLjNG}iqPz;822ynLw?Hb&|KeKEe0iPf#Asx9YP#q;Y2%8KRMhNh%UNw@XwH)&j zz3)B5N~N~32yHfOz@tps@qd<{Rw?Yg%?#STbWLYT5@{k0c-gRmy|(l5BNia^Gtk<6 zR_V8$12Dy+NRhH25cj`A#!X7{i0|JJ?2)ItPqe}qSn81if2Qqa4h{jjf+ZdSez%by z&EW`f6mAYH?W+u_17)O+8U)bcx7By3^P89z2ufO%bq-HZn80>+#4a|L5oK1VoAlyk zE3MjF%Ym5dISh}H8354saToS>Fl(|891?qFYLf?b2OPHr`LUH&cFC$8*U8Jn#NG6U zg7MKAIzEa#&ao6Fb+AhC8h@J^Sy^cb?c@mfPiSu2H~>jM@smr?s^~+vJCdcZtH0a) z_s^mWIP>K6E}Y@djs~y+LDuM`h+LC+l-(km*r;~zSqvVNB%`Gt(Xw{S3el?sRds(< z3};5B1EP?vna>x9Q5HQ&b~t!^ob?unnrg^I zYMD3kXa9c&!BVN(uZowl#znZC^vSn&+j1cXdM>ke2DG? zuH886Qpog%{Qa=Xm`pSJKe&8lrF5mHW2rc#rK;Go%d@wu+Qf~E3bzz=u#b5IbDF?; z%sns$r?n}U7Yeb*j=CrCnfHyjoKN1YOB+tJJ(JuyYdLq23{QLqd7w-Xj*q4tjDF=$ zT_k+^7m-BS>2I((S&*(>IASc|(*={ojq#aj`Wn~J!B2|pVF-T*`6(SJsedIi#FLeX z9N>nF+tqT&Qx*aSyW<)wv7C(PLH92o3q=+qkK>W$2C4)Y6J0%x(u1-Ge0Mf?93M=+ z=Zc5NliKkJF%iIDc&z0pS4bMyRkxw+Y<7fe{4dlDc+{Y#JQmz@qu>vtWnGe6qbW906-N%&cIzz` z1Lgk6o4qksbUNa1Tg~ivGZML{Xpo-(MI-Z&C_0s%7rw;e1^2TwO-T(V(lg|x7oeDf zHm}+xHKl>aMjj{Zg3jWYr4u*-BTBu$^YiUJRI3J-W_7ga+G4PQ(vwf>u|8O=z8mu(-4z$vPNmagTe**R&6FK(1#x?>+~YR4{f9pM~6%I9@MB_ndlw>h44Weofl6<86@ zP$C*iOzFci-Hj!)5Wl)EKfj*R)cU3`s4&|2gKCPykpN7#-<{sACa5Dy>_G zhnN5;1D64EcFUm^`mos$@Be*)1;99LporD+3agrbIt7Ckxr^S=si{%i^?x{M;@GZU z(}RKiorwv03xIBbQd!ICnz??`1U7=Fe{gk3h@4Cl&s#5uOovzuf%2-je~RD_h?S1R zT_T)?x=84jdGl{+{9>O4tW0RUEG+lu9JIw0%IzIZ=*_E`)@gVa`utiUR)L8bY3YZE zy<22XN6pg!P=_KY_t4m+34FsE?((G&lA(pQ3{MbZI|d0gMpX=sgk`W%@c#x6{0fes z!qt6I3#7k$H4iUni49+SG?Zl*5Gd%Q9j31}>odvH4c~(pEegjGd4k~N?r9tTh>qT7 z6db!Z70HmFWbrCH*#z$d7s%mEaFO#I>-Ifaa=r(Spi`s2ihOt@|L6 z+Fp5(XIaG==%j}i%?*XLQ$P;Iysuyv4MMhO255F$2du|W?WP<~(z0Ly>d=LT&;R(O zI1&wUgFONe!&9kM48yyWCcWh5b;t={M;Un~K1C!?Fl2gpmQvWD)OkMP)^ zg)+hu^&LPc@8Hhb0v$V3B$wnU|JDew=cnG5EKIr+EyT0ANCZv1zK;^5?6TMU{Ljh; zHm;I|w%f!68(7c^Ul=}0cl|}ydOHS*U#mw&qYq^C4 zoaMrUS>GuN&rQ%~1=sPYCa}%PoR%trr3fcw@;PBFw2a7^>bW1qRyBdPTS zzg-7Ht|~&7>|sa^UsJHf`IKGR1%o4~P ziOE#JH}fu=O~f2D3{{PrAS+&4^^~M*Nu{O(LHfL$5kB%}!|_pH-!vRoJ+tHy&F_vT z?^0S{PuzwW`XNo6NLq#4Nx~${=*5}~J9jS$_{@2nDdVgmy}4VlH=`*K$h8#MW+{CEE4E7^$f&YkX@q(9Y5+q<^TY|LiRKKnT(UGp*Flw&Ekz<7 z4Vn4oldh#10Gl@)7B7$Fy(VZpYaFapM!V9Nq~UfGd8v4YWmB)mw8n4=9H(-s91qp% zhprxBY|C@L8;-8hGF68jp>rZbyr9+Vka1)>z^SUy+$ZBAfg?a@n z35|;*uHHSCQy~P&#&?Zg|NNmma??0}CO$U!gSW62*-f2sH>(RamY<3Z;2A4bdUpFX zM)NVt_NZ{Pz!mj#x+x|GHC`t9j>n~E)L#JwgcuRFNg=F^wRgQzMvJXnU4aq{cV;uM zLj)qvp|)gZTO%_BkBPgOaxn(HdO$D_j@0tbfU0;#UOEH-<^1zEnB4WE0Yuf};doozf@v(>M)E)1flGO% z%xdu8ut)t-E3C=n+z>ngPw`43$Sm?arBqYO#WqSKP_JC1__Es<$;p}5T$ED7yAo2U zRqw)^)kZY8YLRgR?oDn}#L!9J*GOPzYXuu1R_?q$Rt?cGH&1C)P-@z=UE4guBm}CE zcPBTh1|-D{NfpgE1)0#5A`l{yKO+e`CtRx3Tczd*j7bhcCx$yPk6MHlI966n1O|vf z;J)DsChDSCZJX^nxl177l9B*KOCp`1M9t3fp2X+I(i!;@>d#Kn_}vu3KDW!9_1S?A z>EUQJ56v4j#cgn?4X*5|z#)LF;ZxhJ7^pu1@c(bNCJLS9us@vS{^>?_y6lAkmc+Fi z0$(Yx5WG|2|DK-wIxe1yOgq=RU>S&VpFt48Gj;(VQiboS<;LexSQ)JX!qx_Ps`OD` zM-;yj>gEJW><^5)dN}K7h77YJ0(Oz9POCSOS+T6;kccC&8N&@hb zF*wYNUDMrH`Dh1`2YMyGYys;cY@JwA5^>f5<;a#^hia$Do_gRzfIj){XArcI$g)~`*spmRK_kPRL?qo92IP_J4D)2Tuqf9Z zO9Zq-bthih483M4XrIN&UYQLdcR7a-F##mksk0+2yRtSS_y6m(VLY~ub-{oHt@|Ev zp;YGwL}%N|iQk~!#AB9A_^QYQQH8$BW__dZ55YVcm=ipFBuAUONtVtnY9y|_x6FAw z>FjrCl4|0Y0$yah9;EUDUKY$v@HT{QdSb3x(9;4!aQVhk=dbjJ|5eBWAS}YvtsG1? zJ`5(s0#;qvYzHt8bHjM$S%TD>R#SPBS|SMUU2t(!8D?dbptF9#=zr`Hrjjg>p%g~t z4=I`80a_CUkZs5}-;TgW4}qz%>M9I2sEzsoyU?^)~a0sHBVQpwM3w z=_(ir%#>116)B86_xEdoL~qpvcamfhfl%!ePW{w={w6Y?@mwCzaM=NSiuQ?ytwiyDF96q zOci@Wx;EE%dgzp;9EFbf3&gG2!E@fddxbB+0->#XVjZjRFnmEQ7RgwHd=e zRK)(qulj2?k54st2xD&{l6I9sf?QpIF* zu9=MBHiEp!7%loKib^686&CcWpfQC}24t*DbvE_^L&*>luDE#~XA}T?b159nAZaVF zQx#AkQS$>2lqHr9ql4M?0B<$jsqVJ+YA-wMzV{$n-nmU%X+r_iZzQGd8wOHYfs>mg zNFlj4H<4Ea@<7gC7S?@yC&SmU4KbRkjn9uHt%$Hk0RrujM+UsOS*n zjg=_|_7AlvR{J1rYJtEQ^9yA_)_oMJ&_Cw zL|jyqUSwpyU+)c|Nb+^QMVFEVb&ZEgUbExp`+lA!}i$d(N^I*^Pg7l-){?+7Mft6lR+M@k0e4n?L7x-v@ZD zY}G>0uC%`FQ094x$Khx4jaR3@bQCUv&T>^oB+SpTQLa@1oNbFQ+M^74{mk~e@Bvce z`W*77Z}Bi-H&)tQvWqC3^=J%!1QYNY8|$;9H*4<)LMWNY0()z_pm~uq^+lghPIxYr z;i#wsFTcrwrltz8awu}l2(!XCobUc^a2ps7B4|I&^Dnbt##}P=!_KyAmPeGttGy-t zB`d)dm$ySVOQy0tRz26zj<5cNi|)k~V!>(xK|-~o_w`Ns8fx%Pe$bjK2s8DLw)2+( z-hC<~W8;}z9L3p4;8ya)GFJi`s8lYGYp zs_b z-A)^dCguBH>j>}h9vh}X{>jLg4NFr39`E6v!rlelMkPo~Ku?$%@NK|@viD-QOnCK( z^h)k$RYjZ#wW*`8mC>ugp}pOBWv}bjU^62_)(U3&sxz(%nfH#ab<6__PjaH0P|P*E ze#6o|_d||BY2c`jSC_o@-spgOcEIy-60?r9rW_!I4;e8op%d|ni)=TEPHn@(=4{7 zgj()Sx!J5X*ZbLLi?2@sma;!3wYk&=baRo9O17p7f6DMfvEC%66kI$+ir)t7%%ulq z{AB_gJ13ppiQ;)O)wJs3u)+csWZf<{w~?Q7$!pl)6f1(e6Rrt2IbAvlyZx4Le%y^8 zyPhq;%Z&FG(WlVtyEMoT$w>wmH<^r&svZ5#zZ!?gT8ObJX_IGN@1K>U${)=a^fEDY ztR4B$JR%uz_|&ToZh}i(s%IjWC=0mTpVKo6!0Xz-ON=!4$Fp{k;r*b{GL=FNZ1NmV zj>I5Z;`B5Tv3)iF%9GZdJ7Z+TH@nA>J#d2~MPp@!c`c<*jzB&axH10(azViaN#2-t z3zR7pU^T8G+11=dR9XXrbF(5YOi=1i`!(+Qgg(|TF!rNF8XjO8`RzxsFeHeFUbNd4 zgD>_b%1!Hk?mwQc4l2K^ATYLB{}~zndT)!5rSdr?Os_?CUi+zE4eBo#MTs>L$_}py zv6WqYO4S&b4Go6dZmZ}g2t5)9^kSt`TqH;8UGc}$>zk4o{R3{sHKw5ozS#4~tT7_D zgjv3|gklW;r9G5kbjA1MSnVdg$ux~aa$S?me<36`S&d#nAX0o4CIhV6)FeF4#jt2x zQnjP|S|S(U%F@qS1J)t~<9dgRyQ>;wVdrcL;Ta_Q)V^~LO=!ou1Rl`($|e;J!KuZo zsv~48s-Sl15xo4@^3$f=XgqP;F?q&o|8PI`{`|`gANy?hZTZ&ZeeCDV`WoU$PY(XT z7V^rxgJVfG5ZIwLs||VzNz-f8OgP-ys>a|6ggwSdd$Qu5!J+4e$2QGCfke2zw)W(n@61}uu@q&TB#1uOu z24d7yF<_Fc>#y8@v?t3GxNB%p}*!?M&FR7zO2`=ko)_+HPKZxETOzlE@-mANE;cS=9VfY_CMAv{TkZQ&1akvhyOkMX!8Zn4{ z4E{0P5i;;nijC=w(?*#;#K0oa?Rm#W;$)rGOe&(u+q|GqEtcF|ufxQRCIbj&5;~>} zKe~IL63X=&ek1X#O^C%rGx{~Zw}`m9No~q5+@eKU3egu5&V$@#xyr{r24mSJg#~S3 zQ0<+c9CdPXIp4>J%hlC5hTIw%zWll(-+CgRFA^bD0M!B-SwNW55N&6b_Tk=1E2#u^ z^KqWBF`!Sx0Dx#s^hFpaxdrR=R2f;ms;wn?2xZ?I`K6k^Z6w$jh{d9ogw7>^&)3Bw)Ugf<))d`hMvx7VO5 zk0q7GNQ}G{==t$Q?d!z16J0KS(og`N+{+MN5)H}Go^Oorl`s2jWcT+^aLOV!mMy!h z^~NU{TV-)|Sr=OG$aegmsf>A=P+TU01bRv(PV9qa$(%IEWK3W6BLT4M`YQ_YCYbcvqW%3Ma27zNAEpX@FE+P#P6q(O)uY zp&z$3u8wlL>wt5uA4-WJ1H$5;js4xbQyjU*sqA=X^gS3A8X`^`67ofSzc{V}b@mc9 zW}!u1ikmI~qYYW73x@E>(|>#v1GAm8;3%OhczJ>qQqPfu)=+V25x`O;{FOj>aH_^$C+rkVC&c`cs zGC3#$3W2?08T^!~Y!yh(i(e{_5A&^))W-Onv{D$zo5s5l0iO z6VZ?25JNhxcaH=WBn)Uk_L4Yy=3KKHREk&|32t+NLw0xtEo(3HZDW>P&lmD{0r6g@ zkF(o-+t}w}(sNolskyv5K15L8eIWQay_~_a#&LIwca7et<27uuW^*@Gj+YT_zsu0t*VU(xe`2{|mWL;X8Gp z)sZm7m$+^jY(8@3XYv>do$&#Uo;9y`I_#28dZ`12as;>m<~cKeG<7{c8b< zzMmFPdM77Hq9h_Sw2PXHAmWAwXMu3NsppFS6>RY`mx|lN-N~Y|eYJFAy|pkvc8e1) z6}xp#d7jJCRI58u<*s4`lz+1qBuQx-0{-B_{@=T1I2VFQ_~$a+EzZ2dl?h$R!P_|= zLO~(&GLjTs2dp}5S80$?5v1KL7dTokcN0)v58BnGy1>3-D7KuHiRhTpKER$W@FDsu zkTQWMx-df~LD%YQno)@=3HA9QjA|r`6o0Xhb;fD$R{BgvnzjBFA|h(yW1S_dF2Nqg zz-CEyH_{M}Ek0cZ~2~Z zHUA8<3Kfrq)t)|Z94|W8UC7dmB!B}u=t8C2$*+JXdl|n@(y=skEwj;8qPZ>(U;-q| z0i2G2-NDMOV4-rhZn92;?;s6#14}*@yY5Yu=_#ebq;pm|b2E)1=cQ)-y)`do5y2&{ zxxo_L`pSv*vKAE@>b>WB&w|1qKrDqCZxaJ~FA~K~jY3f)QDHvZj%;iG5&10|VJSlU zCTt!vA@XQu*K`!Zr%`_}MB2}X#8Lg&*Xbmd@#s&io zGtwn5AA1;LZ14*Q@9DG@Ly+GCv``SgPf=6*6QzxPnSAy^)r|31RcC zFAg!BHWIrPr5ZF;SwBQ_V}tMpLR>CcQn6O1;Y!Z17ffJ2C_-xi9HO%Z@jN8enhVX+ z+r?{*IDvPB;F?;YB5I^Bg2R)XUFLx--}@5R2cf{bt&*LSR39-q~hNqsje^vN)1=wdIA(6HYH8*r({jCe@M~W_u$Q&M=WD2^kXr-4_8< zM9Z(xN9AqH17=|rQ;-}kM|iCv85x8v?nP2&$5)O`y^rL|=OcAP1%9^MYn~bp?DV+*fJ?Y%qic{nnyvq(=R?W&hE$!E|4OFt zAhM;KqE=q{NWfpq6zy}sDuHXM(mSBwBbNtq_$||;DuwR-k||i@JIc5D$X*~fE=dq} zWs+>lg)57i6Ud#ynn_8f=sJJYA_=#69D|yOh1Y%6GDgoK>+L)K>}A>*joics1RV$B z0gb$C5YXSRbb0lY8NRg>zTs{f^>(0i3VISsnMdq=I40Y`rPbRNQJ#b&*Q#hgV~;(f z7LPOeK9@1s>2`bw1h8IkzwWMq7%!0PpPoJD>NOF=YCWY|7Yp73T%%Y`$7UTd7M2vP z_KjHg)^QNm&e3tJlT5jD@tk1GxG`ZyhE>Eb;hKP`=>r1>P`|2{6QQdu6}I+i#$N2@ zSxuO3O-tQl^^GvtWSG0JhkxI2Dqnv`kg|YSzT=JYpImRt5czh)42gV*lBVU)dy%oV66l`uB3ENTM*$ph@4ABH zo^tINhiCbWcxX)~m)Z*?0-C*;CKJ#+ij{D*X{Eme2bBFkPF`l_=odBBa%n?@JA%>g z*VuRDm8ipMRkz={TR&|4?}lI3mY<^GeqxHb1)^AV3+v^8P6GR*IGAFxjvgfE%e>>) zj#O{7mCF8_@PX<}tHayyKf%dQeUU}i2*STlhp`II0WCGY=DpCU8u&vmM4$W9i<#?f zSi$Ko{BOmVn1&oee9Ub)d2KLB0k%0rh?4=Ju_BkHVP!mG30(@ePxQp{m0Kb5N1fsP zHTFa&q~pt;h_zrTO*3ER3toT8{nKK-(HmKyZ-ro(IZMPSx#GgTz%XH_s}ftf8kGOb zU$`E9m5J#?6+w1o0yT^9dU=rCwOh(1o`NsJ!NL!Q=wFJQ7WyUDvFgpkK1#QgDt&tc z6c2o3rZhDF5dQN-j9(I5i0E5_h%WxjurFt!Gk1*bpUFXwjzGjX<|F9MFJ^2$j|x6s zJ~~F)2wQ{)oyoJO8H2QJ$O$-zxla30XkKbR_|7{3*ls(DnCxA(6$=9qj0IwYL{0?0scgUoY~QO0|_sz2I& z-DMJs`JLyhho|{@p=^YVnS_Px-- zPuQUc+PE&^FvsrJ@myDKLxt^P2k&F!d%7BE2AJoHu3W9)!pB=;8ukUJ*wv-p51Eb= z@o}AvsA-lOOpV>#%9y}d#x^dzyqI)x4^?WWe-|gAWOKSQSKzh3kkGFvDkk=bwZYU7 zj+*isouI=Pdn3l;76sW3rClIgR0}OykUemi$4MWbfswf36`ll(Lm!cq0MXWbVN}A@e6T>R<SC+b=e*yM&o`@2-w9+KxQ>T{U_--2~%So@B z%KSHD(x#904k!|c$n??+b|u!}b-Ju+37+PYNQ2%cznMep8U2?Dz`*Sd%vc0ASdfUZ zY0$KlArzZAe~#jxNQA0BdP=N%v=jL*9teaso3W@o4hYNw0wUP#XK6iTq^;wdqAc=? zaJo_El3;lDgVl>!zs>PK5N#_KLmb^@YdcLS3^j=j>CiZ0s}76=UQN^yD?-nJWd$jb z)SMg;4-O`r5G+&l>Bv|&-{;GVi18Xj235ubK^bqH^Mo5~N*kRm_&nLnC95?-Hxni8 zi;J7c_wjw(o0;l3=symy3ifYV0(8pHFt62woMicTlc$uy?xa}fBc5g zzB!rC4{c>MS=TvB z2X3Sc#b+#fy5{({!fvH-mnW!nB8cLu1h5|fAGF(5ww3m`F(p*2jC^4F0o`Yd~?#U?+>}dCJ*C@tFTC~3jowsd2MWp%8Hrb26+w8n;b7k zp3DcyqPPeXvOcmn9)Hzzx^m#|Xcu(XIs!r0s?Sh4q9vwhQS)*#eGg<(Bst!y4Bzc2f2;ut4Za%e;yx@{ z7B{zKy&}S~ZAb8K{F_vOKO`G?RZe4ECWy+c6>yJ^#gu0ue{K68`f!{ zG_4H6uFAZHz7r$Xa7vf-n!`&GB-9f8-11+6`HfGVu1qFt@XB0y40%4!VG@|Rc zStI6_jYpFdf3nq^0q8c^{&Cxi)G*Z$amR{VrF;3L@jUjnc*S`H{7;OzS{u{k<>O|8Y|A&8PC>Zbienk&fp@ehQqOvhT@XK z0wiu07P67A&6Vgxk6bz)$niqsg?h#uT*wRe%0#EaNfItZFasL#{$5TR$V>797rzKI zbChfgeALd@)(%?@NBn$sG~^dbC+pJQXcD8HBRkLh-z)|03jcv%Sow4X3)_PpzX;c^ zM4Kq=g(!OXQS29<{PEQcw-D@99KtyQEyVq>{!lmV!uwNVcSv^25gV1}c;kHA>ZYG2 z@XQc(F`4P-lmJ@ef}t#LoW>L^4`e&|sose&b>?dUb!{)17sA8*kHftGNS=u^Ag_k=Zf(Gp7&BSxU5@7zLTqATvx4%Yn=XHQK0 z5ot!MT}mQ~#|;IZY;Zz>(c=$@x{O%>0VwO~3tM?J>}f+OwfO5}>JF05_3$W-@c#_O zz~y=h1^k1}53NI|1so$m{lYQ|#1~#17{uJ-R!={vtuWZp^LR$BTz1z~kq6<8Z#SYO zG&`WW2w!*ji^~42jEEI+!<8j;7pQP!^8T#BSH0a!V#bz9*JW4e?rx4&Md34L-O$1l08!OkZ*3WS&~>% z#CirCwCsic1s*A1N{dUB41d~-%b+1gENSbBQnTH$U1{TFeD~q}PMa^!S29K&m-+QR z6RF%z6kBc~lAq;1R>!M7Be2+gANpbZ^H+cQd8eEd_ZQj7?BMSgE%xt8X{HB6>}bNm z7foI17-aqNHP%2G0KmVhMbd0a>e1ft>4S6V6K-&vN#GN?EKY2g{VE zZ;j!<+=p-*SxVoD49hRblXQdR_=YzlwV-}|@Nsq+{kO=bI4~;ES1xU-vuc0?OwOc&+MB!%5iw!H9A6o+Q)j=U75IJ+8>n;^XzE!BCLE%LLe7@D z)SlS9rpsXd*uECc?!|*+6}USy`eg9dg@Qq;5Ef*Lzi0U0PVeMS$?y?7{rihNrNXpO z6B)$pK@>WkWe)}p*ETM->P?bFzbL~m3=J`@8*TK9vaEm;lzSVL7Ha7^*#dSi zzsggWrbvZif%Q@vpeic4%C#nU4J(tX`P%XGRn6P-tcx|Z?lpNvkn9e`w@Z{#&2 z;g66dCB#l9HmT~P6N6>HF+x7ma7|S^W`3mkB52=2j_K7zsRIMnnw?=8!^=mJ4N~9~ zFeIWRrt4NY48i1wi`{@SNxXnDbBx19iC~iand!WF!eig+PvO8)J<}sxnG1J=XKD!k z%+cz07Qr!UG?MR^;wy~%R)zEtr3&`{piBQ&#pmI7V z7foW4K0pya2sSv?7~X%1ofO;kZVe`QfA+C)ensyly$9S#)?bWi*!yYPmAx$`5x8xO zE`4--ullu!zgtZGF%0Q6}lx3X5m2R}eg0tpx1`P**xQyry13Md2eXk$RIaz|-P#9dZxGVTa4?UjikA|UhogE+V z7>@J8M297T(eSd<+1b(cH^&t`n4Y@?H6_wue3d_i@G8vF3J5HQW{s&=}}K_ej0b|k6f$xZ#sJN)a5!%)1-V*|&!WfTdy$TX`dzD_*J#77tX#WqJgkTD zEHwmUlu$IN0tpz_CE$K3ONMer0q9_2a2vY9K5ursd9fDaAs7bZM^j^9r=hxogcp7Q z(*^oUm!CP4jC%4|Nxdi$hchn%U{Hh^sryF@B}2ITuhDP-EO z47G%uzHVwN9as2TG;?Q$2{}CTMh^7u$N}_P**#`Z*2U!5FW9}7Fmn&!>!RIb7=7i$ zx&G6B_b~f(I}WmfLk<|c;0zV~oBW_7blut!fB2k*5e-xoBQE-><>SY{7BS_J$V9RK zJ6&=S`GxbJZze~FJZ)s!C*h%M0c#$)Qg4d%BIZ=9f`Ege*(wY;1cj6rfg(bWVA>i) zxZ#w$&a!KMlUqxt(Zu#c^$gCXgXXMgV+2UWp=xP0>QRW35&fmumBa}-7PEp+=Y{Rk zB@@`|67*ZIMGP|AY=2BNGo!jADH>!m3j6oJpAm@s6Gv$FpL|M;Po)RYpKie%b5}H* zIP@P{T8g!*t%UA!d(-QD@NyAOQ`F|)5{EGu15gn0?=unUCpc)6_)M|tIowNUMqPm9 zwd{xFrKq2}$^MEpUY)FBfBQ0I&wN1M3E0G`i$QGxc{xLn!SXRp(C+<=<)5s8gUSAL z#cVpwQ7a^t^P-)A0hXdukXC&b3aIg|r$rBYi)FJ6Ieu?_%`I@n9t7zPzZ1lm*n zz9}wt?h7S!?%u_p3AdZ9uCnDSYRt9P=A{#u(Wou%t40B9W$_IRQ}=EaYu+%fg+P}> z6hU;ijjH!!2x;`&SyzNF7n6=x>9l046s^4=R!tVt>@tB8JD2hHR#i@VD$4*0J6Bo8 zs&OW^3)|b;8Kg`fbnHX4iUCuMkN4kxPc|&65cuc@B|ic*6SWEEJ(-9Y6G;RKCOfaj z0AGHr3CEQ57|A*qdYF~)7r%QheS7aa;cb!ij1Dbr{1L!zdRZ@~W~>gB$rGww7nS-C z2FLdGk4jF;mRA4jmHER4I5C(`QTf1A%A2`Nn>}+(+)XI18? z{sr)RFRW;|oS_&b4i>*1$23&ThWq7}u#TP4Lj{gcByi>v1u2*S5Rph4X$CeT$@GO5 zeQL}Bj|3PEZ!20ad=5cpXmsdlm-R*s$zM`a zcC!6Gydq5oi4rzT6;r0B{W+OSGqt7|RUl*v82aXms$-Bdy|NAi?R3fd@%oK)fmpfB z3T6g~Gws066=6Whr0s705_1i^Zxb`cj5!<87J)VL%w_$vxFgP_)J(kBc#-Q(7kQW0 zAzaiH7i@oO%^f=M)ci3dv`+sD$E_~F#;Lq@)&J)=!;rhXYh{3@esVOg)x2rRf1nJ% z3Ez_CkAyzN4qbdA)*)cP>e0jECCJaX;k@hrQbgh%Jrj3%@q;xd82o$K*4Ych60?&R zru=%_QO%xb31OU*s^gKX$};+^ipV^GrVi>?VBv~L7=jj?U)PZ4@x{0l5!0_dWu4e+ zG;i4#MI>5#XSmjLDEM#lLEMi!*Ljx&@Ch{`DiSoV%$Pcwa}2qd8Ks~E-p?vn325%B zz4;ViL1{IejWpy@2KrZTMQ7<}yU{&?P%V2hvmKmEbU*tV3VwC?gMGv!AjR7l!>_J; zeL~6|oCD6$KlUf?BZcWe9Xl9fMj=>KGOP{kCo)Ba4LJP~F5<9YoS(0M&_oQbw2QS_ zIgbot(}jfv&`IqosCT8@KbUwl;}F0X=3{`i4iyC25B?{PZ(3$AkENzsdjsCDgU7Gl zm@vGMbk`A%gM}CfTR<{ZNLK_1jzE7q6m>D)X#|l6YwXN?Hi$IdL0evU?rwnE#r$A) zoe=V_2lvcqKb#HqmH_o%e3q=B5BhhBZncE1rHJx?-pEPOBU=QrmtG_secT`mR(z^* zsYHj($ez=bGsO0jy%M?a5mr=1G6_lU4#xU-?!ueaHuE!)HXVjBx{zlfY1LLEHH7LG zCoEPkUG1cI801=d1NQ5rfuckP9OWbNe^_=aFxo$pR)W_-+?zqe;A8(GRLKjVbX^gm zR4b`8{nd?r4Ds?s!m}soE;^W&;n233Y?d;>g&g7Yz;If8zB8^7nMU*S@VqKrZh7s;Fx zLZXVAa==#@oI-=yGo!jGA)U7ewWsFyZ9R=Ziz~2>7klPWpdvM13V2&1DYp=qeHktO z<~7X{xF5V(qnKFrWpitK*Cs2{EXsR{J5Q8^#Pa7e99i@&55K+=G=>P28<|Z^0gg+z=bXKp4r4v(gTJN%?D!vEj#F$06J`DN z(HAV{NS+=c5{TWfTSs60Bw|O{AwfBC`n=8_aOXUVX>f#Zx84Eq@lp& zt8ojG19;A>vQXp;#Th0aaTeIw}`KEo{=35*d~PC13Nz{ ziV|H~O6cegD-QCqa~!Z7i$Vo+t?kudi^gQ$qG_}pUUP(_~Bn=&6B3A3U_T&()%%7+bVSV6g<$w zvjvbPU24)8?V4O_hgZ*_{P52;%~r|B1{+?twghY&5s%a`EosJPbZUNG$_o%WI{7g8 z|5x_*?Fkr%$6KkZS``QuHUFzQ1u3NWEad#YM`KmmT*2FIuvZZF>JgCZp= z%MVP^{8hps&MY~CEKkt35d;hAJhQ1010_3Z^xQ=D+Eqsqig2Q!!y$5yEqIOmhJyP4 z>o4nAVWeWZ)j_XM_P>Q;0FT-_1s8%L$}NjHMH5Bx#;OkqZ{8A6$XOYR{=h2^e}d7a z+M+saooWD*YR^NiVN5fW61C7v>c?nm{4e9iX58a|3yM6TY7zKUMWfh2g?c(lzkn%Y zk<2%(!jvX`l__-e6WXns!i|jtw2OHo1xZ%0O)Z$t$w=hCX{cEjt{>{G$q0aJZiv8p zIV-sj);yPC73qOzcqv<3C7GrR{y;3pX!?x)Dh+)r(I1PN(tA8*tVYteG6a+&-+^u^ zd3isQ1fhWHb=q;`yq?WmE!~eAhI2}g>Gzcosc%sC$SUdr(lLsk;p!W*n|rl!TU-KO z159&*El1HhovL8JFo^!qMevD2%P-*H`*G<)~{To>;e}szmV=EJ*gAG94v5Flx(1?{|0j^2QXCn|e z7^DWgf@j2^E?d)8-rIF^Q~!tYhiv?^x2kQwTL6%c82nF-(g$&v5Jd~{RM%Ghyta`3 zr_QyX=)Tq`2j2z(C1mVH3Je)5w)IkO&9yp$e2$?p%5S; z)>GDGN>e%_HwPi~xj88mcJZiH%B$EPpfWcVohCV03Pq|Zu+xVTeCZ42sa954HJmxI z%ztLJj2j&MAmd<}aU|&Jxw*yg&|6aKfgbBL7!rgdkTWq*=fnQeN|?4wd(8XV_cv{* zEhu!Y|0}u<&8`F=#M*CbhH7)Iy`Vkx2%YzwenlfGgHDEV81Iaakxyz;-HU%GeA3x6 zu1qjsL@&Wk4J)r4gb=eZX=+N)3oK>DKU2)~S+HKXNPeetaQucp0&K05(wkVobw#zx zq`h=l^M1t*T8)6ziZTcm6Cc=F=a@>He}}lO=!rX?z0gO(8PTXcb9;x*NsBPoog_OG z4WIZxmA5b%Ur`^P5^%+cWa)86rOePUaI=pS&CmQV8W|GqM1cfBk({ooYZq3mt;6Tn zH~T57F+Mjn2S!12GKi6*5M5gQ_-TQe20zWqP({#$6><}Z_q#yjNOCtpm;90(DSKQ| z6Z>a43ebgK!9HD3naUJ^64TQyx(WC(!W4V2*fjqdECvS%iQ(mj|IdlD5^stl9OT~);_q8vVL537+Ph3Ck5F;yS%h5N|e`w1n~91 zvtOJc>(7c06=_(=n5@Sm;fSN-i9T4*E1MEY?3MjxQ1IBH^9kdEBL$;L1&X^sQa5X; zu=l*}94s(=NzJOkr>90<4m@W#AMG(f84A%T99vQr;R7a!<;%{35HfmS=BnuSLTPa$ z*iBGm8W8Iss^6wX7@>6Qa6coBgNg3`<6kV;1v*i~e>Ba-7R?X&byNL|CJ`d%TG)WQ zQ`eJ85g$anP6s)}usSnMCOY*AWX;-c(r0QFm+v)$5%lE%i*G~lNvdq)!+if&$9W%R zZGHmJ-Cz7pJBL{qCy9Qp>@l~?-ck?Hw2B3h5A*OXq3<0D$4U&|Vxi1ALlVoydPn@O z;|`zi>a5@d!SqK+;&}|*d_#ab%B|c&5+!lI$9&TjjdOq;WHb|%s1P{Z;0{h7IwOe5 z@8l_ztivI=(ECA!myrR7^eg@r4G>pev&wTs`GjRCgoIjnzQc66tNQ@!QLopwP|vGV zrA^5u_I}UZ#=|>Q@&K|#6O1t=vAJk92ztj8!Zd``n2Y6IE{iCtoD$eXVz1tQE|F^! zzPWI#S}muW4|-+<4YOB9jVSWMA_7h~MdlfEHt`MObed_;-a=b zU$C@O25jItYRTpl3|;{hvV656SMIyD-F^ZBVHe^3jL#)XiM}TAYnNfl53lne<}#ZM z7)*8L!zN;%v2?t({5w~RFWbok4vqcL)l!6eZx@Iq^wCAq58M^aC5Z?jrPQQD5H@^V zG0ip%Jm72fv0LNjP_Y?yKS$22(*V=10y?X7&AES1ZUTY&f69iX@tYX}0uVGRd-kJR zmf^NpRd_nb)ilkXv?z9=<7|HIp&4jbgOR*izgfGu>p4!1`xO(X0@55w?76671^D*1 zkjC!v7k_5=e#1~T7+K{d>>3pUp=GU|IU~lizwtj_TfSYD5Oo-%s)82e`TJ-Xf7tR_ zd8xF(mE6U*=ggBTwMy_@m~--wxn;hbziVyt!SN#h5-yLCxZEz6NEUaLsBCjl+Y98P zlN1-LiP5hn-^p9P^U)~q5qqJw+ZG0?$y}vN2%&H;ycHJfTG~uA-?iBf>j)t#5@y*s zw#+)GB4iXv6};hl1U?tO8#4W(8I8H{1sH>YXES|k#N&UHlz;mYOMw2+eWw1$fjb1o$%*4gTO(^&W zfOyI&m`7|dbnt2N@cICF5fPJw;e{w1)X^E0aNwrxOlJ(gvQdc~W?$dZN|z~ZQsajg zeS2qh0FE7ETFlp04pxc9x9&CMwRLjgL~|Er>lg*v33TXOKrx6(ZHhNP`2_!B$sT{Q zf4kw*NAl9Y1n7|obaOdE1M-j(uHVC@TqTS5k9S2%in4SjH|0N5dt%^SQ44KL;_pPE z*I^E71kZ}9U$181PD1J?eqUMi=5m}D1)O-#$9%UnLVUnwEz(>vD z0i%kohmvvat+`T$w8IYtf{&QgrE|0Ag1K@DKsJVJ(3i4#7fV z9BpdRE7m`mS72axVZwTB52w&^y7Y+&wR%-=U zV1Kjv3Gea|kyvBI-env}#J$6wm$kaEV$PQJ7qmxl2yZI4F)Wrz1a%aV@%KS}od(7J z>S388!0(o46(n2%(*3f#A;YtDDBb~lQAYO!=QUo}LjgNM74j+SIOaYs?h_eI?S^83 zp@V+kg=lg`B!A`|RALXm64f|Ab0z_Qe{Rc`iMZ6Ll$Hux@}(NN2(E`e}x z2|A-HAi91c$8rzASo~p;gC6>AK(!A1hIwNt zzG4;i@Kwr$zW>-8fL<*Q@ju^gKFcOnFn7*FI<3*sNs(n5v zurKrJ)Nz76Aj#umnsyio2NGli}nheQ`h)%?1-`%yeP8FCtgJru3@^@kIpGg1qeK88`PZ) z|0PtYB4~j36Sg4=afpgJinI@VhckEqjzO+CMAIRtA8o1j6nM2zcCsJ!=#cE{SC9s~ z^Zo?K1zT=DYSjtacJ$7|E6N^VW%LfSXW&S>w^F+1>25Zsf@k1*d((1R?QJ{d2&guT z47hQLN_kk*ZYsGy8U0NXsb*Gn3>OKqVo-P90VxOdJy4cQfgM`fMs})h5uvqiJA~~r zj~(qj4tc1at(RN~r%jAjc0?_}1s58w`5amS<2Q{y*^JqfRmnlJ6S3Ef0A2s@bUtpy z^wtMfo^-9S5x8xNcx-Ghx)u4TJ-9I;>f6o?xLasiD5FHRuLdWDjtv*AEDPF9x zJ;Eq>nCjiA-qyB53kQdBja8C1plrAgpdA(f~5_7$S00Ot%_7So7ZFZ6yr z(<$}wO724k!K`F;krL1ax)&mA2}VsF+|wbhL}-}=+ZY1FtBe{H9?$4&VXlGLj<=QH z0VFBkwXuDim#iXDI>qSh8MWesbuDfEXDpeG8+&&Y>0r#&KdaB({V{GGMDKx@D6)4p z1M|}PrUZfnS4eEZ{T+F&;J^5216;5t~Lx)BZQT7*%d) zchI7`Steg8j+)e&#ldKu4qM?gcb3gn^7tuN#Jl@xA;&s|3+abswkCih)h5ht=%3{6 zTl&VcQ!WYSX}>+MZz_lCn`R0f2XXl9#VCYB@5be35o(23J=FgqnXZxjKW?)LA9sVz zmorkatM&s#dJD0w6=q z7CsP!V+Z}wY`6*@K9!lSq|bSd!YcP!2{*}F;>|f6iEv^PR8{$ppQ?GsILXc zg}D!`@e|`o{>m19b@;VT2bMUd@;Rd1X_7tK@T&3a>`FgS30KMFW-1^JW`kthU8dPu z16Pj)WdfYEe1=L&1u&u0g+5daUq(qcZ5RB!C2QR13()_XMa5_J_`wu&Vzae?CpUVh zoj0jAaEMe)1Jf)}!v3wGgr`$G4xYXc($Zngt8Wfc(VO>QBuE~3J+!p*SFPA9z3gE& zR4_8M4m@pokt#%FT&)ilk{IK#{W?>65J_jFBrTE9kl>RRkiCET0UULT7xd$TE^DE! z2#7?T^+|t|mOI^Is17@N+2MZC?1%MjMB&@jKrZ9-dP%bFs6ZTOB2-Arg-*a=4*=lM zKlt@O*H&bm80K;_D&S(#u`O~DjvW|=ytxoPnf++f+20S>Q@3@Wwwg493dE8S?INuAMr`(7QoL)H)haD`uz;KWcK>D#a~Uwc~9IhZv>~G3Fx>aONwwZ;K-}& z<=fHU1Qu$))JV$(Pw7Dv4p+WQHg6``ogtnZODKsP=Kzp=3zJR#EuhM3+Hh&_va*0T z-`fK^JW|brr8&;kr~w&r8{|h?M9%jkL?sqIT6ow#+`pUVN7U^1uP)pc~pjGcb{+)=yakLxm7CMi6T~Y9$E=~rHYhRV9W!B!Oh@W0e z)YJiQJt0afZg_(J0$Efn0qO3m9<&3b74@psq=<|m4g22^NHre*l**MHWeA)M$|*ad zs?k5C+D%}PdurdB(GIxTG#}^x1efh44euewr2U$l^j=`JtTS&C;7p7>NC=`c0>M@| z__4Zdm!Gfoi|rfgUeaEx40fqIq{0G$*9i!8%r17b=v^r3rQT(UaifWV+fwJ*W&#qH`A{9+pkat}A^GO+C=P z0f%}#AhEAV0M(Q}I^FE7UDg4`Wlz8>OwUA+#(@+rJLl??Icpf!1M9Cq_UO9uoO?jA zm?G}y8NsQ%A>C=s0o-~@)Zdy0P!LvDj3Tem(r;I>5}%KM zYxx5!iJ`Zyl|3DVvYJ--o`Dw4XKpjW23WPqZ!;Xq1q*hk_cI$3_{eTv33ZJwYI)IV zI#aI}bpg{ZmFj3cwbSk(o4Jr1|G)SZ9+^p&vt%9AAD%@m!sjkn=`$2&jO2ItNOTGChS$Bgs(D zSQqW9Xule5WBUe2c7LHDyE|)&c}+;ERr)-AD0=1{OO;8cj%wf?Dynp|alw%B(WLU* z7e$!IHtgp~8<9?HPL?B8MvVnt25t=~53mb2*duP!YP%+KWdWjEYof|(o;n1Uz3T}P z8*_!c#rh^FvLNevL|Gnj2z3`>PUDY;o!v7tBaK$P-i%dG0aR%ZIYH_hFz*F5Q7Dk5 z%{LQ`@ZyTNaI=3g4zPh%kfo$H0NzeqOMvPQl;+ z_f8eNwB3!=1?$+q0Dy%go^34ry9z>=F$(R$)lyPT1KXdSY9myr=%lm=^--*;XNgQM zy}&A!3vB1;WyBHxGaz2K#R}k`^i?5q*%N_C3;6z0&eRl1@J7y$fc0r6nlJ(;*bB$e z7`i9HGfNmhs;j&CG-WKIqjhx%FM0=iRqBgClrh4wfgrVQJay1;BO4h1W~H`51KN}i zR6w-BBrfJyr^?v;h1te9Hg1l(EGJu$Bwn2jirht173z2i@d}&4nyp5y7E?Agj}W2E zWIj3DB1Oed$qV(r-&Icdzs^rfWIg8F$rNP8-warWuI{}A;oT!N08@FDK_{kRuVq5o znmzz~m6F<&3;eF-j>V)Mi3#T-g1z=9MZ?pw*;J#h)AAAz1@p3s_&LCnerUL{&_4{u zQ{dSEU_V9Nxz6Cv8H=^9JZ}CK%d*mq79yukI&WIq6L^ir^rnT5^6-BO^F?o`$9i)D z(I7f3Z43T9c{<9g#>yGA#I5i(U#HgL7vI|<5J)*1*bCD%JtzxvFFt-{D~CN2N$&Oe z7Ud@8d=}*Wxu9wQdw#4xA`l%8itMHd4wKlXbrAn%Qd&oTojmAE~R>;74G z+%+R;()Uwf+|!+Tgdh!GRn@(Z1J-kswe7_qEhMELPgl&+bqv$upt&`BgBQM)MN4F` z2mu|{(2#r_Ch?5nL8FyV8RE`*a1cg%G6AE&u`FUKo-##$e1HM6y5OhUeyN(Uxg??0C26W#4g_G#Ffz(vE~Xelk8 zjmSxBU^3u*TbA;~rh%CL@G*YW6e7r^fCz!r^k@#K3RKg0QUSD%7bfp>>f)`Uh5KtM zhOJs{t}+i^8sR3r5bB%j-$~HcHXx6Do+BSo7FuUwpn1TeWlr82PtX%E8zqx!!ajfC zf4n*JyfNOaKH*IP9^PF4shfj7Md@0jOl1zeY-J&!53d!pM1Z&EA z%0ihP%(p-FYu1U=$ZQ$0g~^VY40|+-ETg=_YkwLT>$K@Kg|MGM`>T^^)s*{)U!5L; za%l3wv#q7Ik5yedlB0I{5%SgWv4c-M8n0tjILP5+wNbKw* z;Pjas2nZ#Q5rmk^rRIk`qH}$wBESFPP3QZ`>JHEFEm2_h4~!E{%<#l zqY8ln3RtA~TOr!KKP^5cHw|er&K^w0)V_hWGi42FqnI+9l<-j!oyLc_fY~E}E8H(| z0i-gv5_XU?6fJBCbtauAX$RxaRueXNwG87iWKwOKmX!M~scIIcFm7&<#R7|G7C$%{ z(B@4dKUbB$V5g4-e4N(61jm1%HMsnI!!&R*2=~hoR(&3O3VMOSXC$FY7;R|eAlyuz zdYYu9Ra&@b|EV>h$jb`SG`fgB73J=39Sg6J-U~mrGTJW(lT%LO2cFI5$CL(@8&%ut zNIOd5S%r%0`M6+uy=W6fZXU)3{7L+l*Q66rZ_0~BP`SoP+$yz0my zP8){TO~ut2+239u*f@Ow%(f(}lX`U1AV3)5(OIlK6RnYhDnkX(z}efzbqIg=l`g6$ zg^l}089Hump}~1=6c6Ph6iv&9S}trRpw4l{6dyC~=@HF$Zd+l!dHGNSV?U-JWPSA- z`vKbXcXgv!m+?gQrwd;sO&NL@N3LAVhbX5c%rDl%BH3siCNiEo5;8d`!u|?1P zQj3sYAyQn+S<+?REPKxevi)=e1kBBYN6gU~a;!2+Tm5y%x{Q8;9~FHaP701;xMjpA zlQr9tZ%=xuP}%w^)Sct^4eS?`lpe=f+Y~Ny`4Ur3cB5asiDLA9x+`S=4`qO4)MfZzAnRArR^exAFJhCH?QRniU zMv>7mtFv`86|M^}(WC}au1NTuRXk~gRNI$go%$)Z5Ef=-r1j6?nb&`#WbZ|WjNc@J zwL2Qy3XEwU(ZmNbRL`r}t_Q>$tz(q^{)jH1rkZhVSB}si*qEh)L7(!~7c6nZwf=Is z4=X+A_&q&R-A9_`7`nb)f@n&B^(aJ(e>nkKREh5ykAZelfp&E^^TIX6WA~!NoqZYKqax>3${X+j% z2mO*(&^eV>Q;gt;L9pf}Es4fF#P@M?mI1?#X9^YPb!J{dpAV5j{AYOR zATiOK$9aW5!BUhs0I@g5nW^{WgWpkxr?kFz7s(iEsucJnl{e+qv?SK*W`PgMAx^c` zT)FDajmS`iV^3{WG?Jje2>9smXcxYfk)j9JR|rl>a+H+^_9{Mr=9W(lIb%D@2`E$; zN>B@`rb2(Zo3O7Uon(lTj{E+Ko;k+watYfeB`)@<(jP5@Hf)bISP87-a70cCg$4NX4wot-2zE4G03J{oWwsJRJ8BCNR$Q;V;1{sI8`aOQo%xil20q z6QV`rc(4VJjr9zZgU}8PAqN?gvlV?Mfz}9HH*xo%Ht%0b<0IA z#AnJcY=%A+icDZJwhI6IUl(2Zx1?$>SrrXQrgP@|)_$qcfjPSvVLbb}n#tH~Bi|Cm znkLkAH-JVy*aHGdcP~n&tL#tGZ!U;SNXXSjw_xncAS-PvG-U1``y|17S`9FGpMnCL zA}UY)DHZ@c%ycpLyrf&JB>#(l}78LG% zG6`ej0Buqw%L{^5bv&5~r8EgN5b1Wz-bViDF`jA_2#dSS|Cm$e;0Uc?fsW9EmJ+du zhSFmG;l>?HIs3e3kh6kIgGfp7Z!(vwpUXzsw|-#aonD4^PT>$f7-OJJ#}f70BNWcmuN;fWDl)KdHY!L*?)bM zxmo%(dvddjGwc>HtUE7m4DC&-*8SX3U{S@v^T7gT5#DUhVH4EX-Yx#A=?Pl1r>8OX zvB1`)FNaWKfRv{o8uZijHq5%)ZpL*Em3gAa6H^e`)C%S_>H@O1)_myNQ|Ovn{?nez znUr$&oZN8U=qy25w#|N~L%_YyG5){`0IczgOAJxVp$w$W##`6einSC+bW1zMxA#zp zxeMb=o?Ve7q)z&4oW%cNWLsS%P4+ad%MkuZJ zk9jmk{gv9*gB%xWQ2+DDDPDH*=on8GDPG+I4J1IOAE6w;V~1`sLZ-34*FpmWnl6f3 z<|bx^CfG5^ge5v87VR^i4~ho@+yJq+ zs|8?LYgp5;5xQJzlUW-YHK$__WMzs4;)p8>jl0t`>3F? z+>j08pb{$6wx5rTS&{1dsqgj%azv{>a{mH3!9xUY>C(Ac?7|mH@3udz_62klHef6dhHSILNCs|Xs&)DpBDSv4|%dD8Sq5YI4Uzh`0kMci9q(3i;ffYr0k z>}AJcdf4cE5_um7T`7648X477MnfMI(ymKs;>@Tk%ZZbkg zmPq=qQ^j>H+ztZaQ5wMdb@|veTOllzO=RBF+651&T8 zdIZ>VQoD^TzBjgVV3XCX@~1$7w7u8PDgG&oIhET-o&o@sI?rMwmpXc9F+WBX_d$CO2Y35B8FcDe)P~FaB9fM-ZLOcX{f; z^G3lM0J-;9{|l`3`u=%K+uFA-AWE)tEAXyMKM2zwY}QQSSp0S$$OCC=hF}}b>KOKP z4BRI`Qv^qdAotqR7>+m+TXq>$H!3r>j(39tqj_*Oe>w8TQ!uJr_@yj`nYPfIxi05 z;0{S)d;QbW_pbQ&8I^EaiiI!ip`;JYV+Ch-qh86K6}vovu33D+Lo=hp%N)39ooH=c zN5?i4&Rb7X0`TTgs7M znKmASStR)7aGkgtr*J@$WEVr}^F*qH+rOi|~f!4a6y*{v%RRE#3%0~Sm$K&g*rFNbCQzWr1+09y-i*~<9L1Qc>N z3B^c1q7uUSIS13lHpV?B4>9t6+x;byZ}|c^E?XQi=(8DWh3Ri)y=W#Q*K#4+MTkNM4MERmefoUymN& z7U;ITAGx8`Ab4F=t0pz#;g9k)G|e(g$Hu|=ygCR}Jru{quFRBXeh))TeLc_4;E2Tz`V-z}=MNE%cnJ@?o1mtTWew+Ssxi$Z;#GT@&!V zp99bCQPmJ&KeJ!N+_*1ytLts|gA@tr@U_!1)xB%!wWb%-sA@|suP0r*2?9mv8U|7O zn9x?nIDgj3)?!g;_@(??T4ju;pi&$u;D#B!OIK3q@Ds-h+fuy;Cd}2<(H!d} zYu5fC7Ewu2RDyQE>?-7+#J`)G^az33lLH;X`6x;qT1i_yz7C-(>Gf&~lV%FzHkgmE3RJY?ffm zLc8w6iF>gm8O4*fd4CDiMD>39r2u=rSO6JrFZ5KDFi5@jKG!X`|8n_X!_4|IP(Ggm zSJw#!U+ojD-qK`}a@&yo+ehR>@(vOI{>nIdftyN`h^2E~Q}LouXcuGc{5Fh3zc6;F z(*8qIk_?T1OiKK&H>ehZbr0|9lT-+&ErHf0DYG7duZs+6Zo6Ve|G_&%OHhz?LP$Wg)Z_MVLQ{mt8AyST4tNGt??CGhcGa-u^f&CG?@-}B+4Fad$2 z_W<;a5eg)dg$=<^$Ivkj+d})33k}ErQbAx_(TgX3S9KY7%`PiQhkuXDA_cu_{o0Lq z#_7XKr-#pydZRJMa1-)|+ASnVl+k#+WDkNj5=ErN0IMOcDicT0r7B9T7P`t^B9}vIWg#QRdu(l7eQJa|& za8{NCQdYrw*3(N)S%6tU6zOAXj@I>iB{|qiSFkAFR1gxNR_T)IsoGG0#>_rj^n?DD zl1*5>Kz3=uXXYDykMJtRSYLA}{QoWZA6g4QRSF495Y(QWB7>96AHgg8Xiol?v#AF+ ztua5QdS?HnN=V-=UbUZF^PYt^XnJ&sOjLH z_~1nB2dmq09P27vM(gi=flaMPQ3BE#Fw@aObi7-`2dS$YywSTWv~`uLu;ueCZ1D|3 z*_)L*yoFm7f`vj7#Cn*oISv|stz{dpzPbZ?zDNFBGrHTZ^2#zrfca=`>vK7aWL@dV zAXfa9d<0h-YhH6Dvk=kS@!Ie_AS03K0R;NNUPu~FJ^rWl#OjFnF*Dqn zN$56y^c2(l-~lhxjiQpd^ib+m@AU$Y;$(hY?Nwsz)F&f%r_A|IauBhX5}(CENOg`+ zY%S8KF$;Rp&Y;fy8e4w#%xw*G=dQ<8EQO6ZXMH=urY{$kVhD76?~^L6H^cKvPJL*g29K5EvkPkC$rfxp z^O2mqR*sr|O|vj=l>eKjI^ovn_NU?!Ffp5m#v=kr`e$O6(kc%Ftj;d(gEQN7j>>`LJ9S8P#+iIw(Hd;lb;IRiMXF$>);tC*fY-tTLo;<{?le)*GA zK}F;>MwO65-qcK@+tp}ZogFjtrVmTq=CFggnQMjT2p&bvZ&M^MTRU{chD?jQ1I_E; zaAcVi;1i-NMyoADJpLZ}o)nVq&pfnb)=wc!O=YB_Ea4LG*I<^pG|KhqDc~amE`!ZB zcvH^JNugCZ)aqs7>j9RM3t-X=XK*%b(qghe`)z0DKd;9)w{-r%+goYI)1S=$8J!u+ zE`2}-bK6Z#sRU#96gYjC=^Z9mAD7D!`_jMBsNR8pQVIP|vZKJq*(!p! z*uW|Tf=2gBnZGD`nsJ@YohsAv%_ zB?-q^5c2VFRciMlnldiX@}UwS6W>Dh*8%iaH!*!Xh@e|*OF7~q@%{Xjm+zpSr^M;N zL4VAJ>BO6j=GH0)Vs4XqLy9)?o((3x*E3@iFg{Mkh=iT6%X54$1-2tS)_UTJeze|9 z3+r7jI?p~8pYZI3-Y$I3-vsDk;6c*KECAAkM{#Kj$x+uc(gs^5;>dA|F@{4-cM~w{w5N;1=TV$9$9LxQGIvP z!2_sjj;Z}DC29S(9+h|WvpJ4S{!Nkk=AMo^XjxxPZtricP;#w&l|YIle;kJ=tO4dj zW}EqM2f#+N=?fWJa0PKif2_LWuv+NI37#fiKLJohp6o!u@hJ;7j>J!%^#)2N&A)xs zADgT2HZSk2DtZ#YDuRQjrGZ*nyo#qq8;56I$ zw|-KxC(XjnntLfCUCe8Y%F6W=RIL0cpl%IsM1T_~8A2;!(+TIt3{Q@lW>s0WIju>f zU5}B>FmYWajACrP2(>ddsfJr1+rC^iyE=N!Q(AmM)evt$s?DHk=kQDMp^Jm7WuMI} zEVT6Yz}u@AkttYDwoyj0t~bV&iwQ$6Z-hu1D*`4{n(7$!%n`IMR~Abno{M%NvBV{Vwe^lrzbtK`9s4 zU87vJdEUh*V9w67{lETm_)VyI<_8Kf&ARtLG`z+zXLOoq>FYINoYOF9k0==TJekb3 zY7OaL;d#Rf)ogf+|EZ}C)dgQW2HvVaKf;dGr7hs*yHy1pMwx}e&QU2X;tgXxJ1A7t zhXigwkWu_VT%rs_D+#_?A5?2W%mo9Q$d+13_jsW*$4)=73Y?zp(!ysFPKh?O zy1PmU+FG9^g$#j)4(o`S|Er-g5-;26##jA-I}=Qt15SEYQ=eeEw^ROUF}y*7T_XMF z)zY;PER<(S=PXYi6^8n6-(CHxF7Rrl0^?>dE zMmog&%3_Tt#Q_yG-uxqEgG7qG((Ca6$?BueSCKv7mkIXhGlvSj%1C+agN$%~g~voK zJ~NXQ(f1m~^)Eq{?@BeVwlXlgjZB9oV7FGv78qFB5liz^HbI{MZ{uc3boO<@+`m<= zJpqSOcMTfkQSHj8ZVG-s+e;vBBg}g}5((xcBoW#VVi82%G)_N#u95@RBb$}Y*&PiN z_6ZVF`f|~5#GtYc>qiyXH8&(*st%2(DX5v2O4o%pI3ed;|Ic)M>)0n(EPKBwz_hK# zgp)G&(aI_CeajI8Ph{da#uMPX(+&kBnOEL?H1|i8@>n}#4e-_&ojb=;*EB_rclvE{ zk)9KhhmN%E{cfdcQ4$eC0N;u^@;jkwJyJ_`-6!<4F!(yy9brq+WG}qSuO~0v^s)M15&qH{L0s#`$rU3xX z4_tO-m@~(q_hoc;^*#OWnj1CnrD)qwtq^wxF-ADOgSgU68w+(MwbQs=^$30+vZtAt zoj3hBcGV82?A^#uc-O2SUhkjvzsrxBD3LrVmvyezAfB?s*|2@#i4wFOPh8|o&js}6 zutCpCy!By|F$G{11Z0m36WFgF1lgN7zc`y}2j%-`XNxz+UGO+m&FWhz%7jLh}q!xmVy ze5@*ri=C2eS>jAE=b}P+NH+R9w-h?9y%_%EdNEN4tE5O~p%v8xy$EDn`9Nu|$v|09#K64`dyBp~IJi_$yT}r?Ws@@VmT2AA zSivr;6;&!y85M7*@15J$AW%WfMFP?r6q_BaSI!9o5L`UR12?bR4%93WF zusXgyp6CSMAovW&oq~29%sbV;^5e=aOe5BIz5)VVn?8uCcUQ``*%9&NyZGIqJ?YBb z5eQMHe%H(f-Fmz?ed_&fi`VtXk-U0tq=rALo4m!-Jc&^dmJLMud4C0ZGd6MLS~ACA zKvwv;drdKqu}8+UgBlcw$T^h~F5A!k`NPotriMyOV*mno6%@RZN1T$}db#ryaS==F z16n^S#HG-*XPvkDcQs)Xs#K_I8gHI8xjEodoez`nM6H`7HpMcR@@e1+m;H(2RZU(~ zRS)E1rPiXw0t^{{RBFCMN@HPw1{3I(ng@8U<8Y|I+SM&K@IIt7NXEgegeb3arLR-s z_pSJ3ZEhSQ3BJ=8t4Pd=q$vtZF&3hW)Pd0i^n!4oJrqn{Yf3g)657wZ?aD=a-)!4AEN@Dpy_ z&~&vhh#0=D$s{3U&n+U^Moo(LhfuYBS9Y>nZa`@l7Ux|D&^b1C+_pysp87R+boe>9 z))@Z^`=;U{Y4Is8)HJSch9Wvpxt$1aaa`Lr=9XLRhFx9^U_GV`3D$qDQW?sc(-$CU ztAs(;<&l(CoMPnN{;iZKsg0;}gYiA43c(@~pz1rX+&4ix{+}&|6OhHsZy6^-5EOFy zVEcXMW0SJ2*yM(Im?W=5C8#NboY-gaBSR3lyGgyH_?a|t6*tk9q!FCuAX1wgay^Lf zWP!eIW7M)p{b6(V#vdb%k#Nr};3wqK0 z+D*Rn{2F=1lU`p(3GtWW_Uax^v;HejtFTZgKbyfAxB{9RoZBWrKQPcuz|UJ|hl?{* zFDx|-T1diVR%!&_N5G%oD)7JkESdZ?y=WBG^bGVxa-sCGYYo3aU^sxC#_W|*bK7am ze&$~3SC;E6MSCpqYQZJe$uI-bJxddUAs8J(&HTQT6+>`{IxZhg2u2}*Tc?V@@cF_n zflxGg?!;-in($i+%lTz#CsX&?CI$1pxHK0H{_q5ZQBnhX-e$YJCCnrOGwip))()jb zl@{6xPnRjyA-$6q(xUjOTmtNz@d3k;h4}H^RN-FF14LQ~C0lH&qUxJwQV8opdE_BP zi$^k+Bj!k^0AxDqj0{I`{yWYBYS(mhdVm6$b<~7fDp@}CE4-xwG0Vt61+a3|ZdR&R z9epN@l-ZH0v>38UAfTU!W70mbIxS-UHAgB7dOn;M^|&hTh6x3eLO`}E{t@i>#@unV z{~^xdmK49mUNe1ayr{4Iba8O}G4Wmt2AB2GJ0bRwir(DasF+L>u8=#u*QqlvWjigN zy9Iu4FVZ(TPffbIA)Qf;DEh5&s{I|w1X}0KU!;+V-5vt~Oa9YvYBG#t-KF#;q6$vS zL@q?=Z!L$87F}A=yVXxb#_`O&uQK4~OxyM4S&7}SU1oGLu38#i*Zg6u`~+Q|s?0rE zMDwc61xfK`4+H;;i#MZaQ=LiX_dfGx!0<`|QafW>Ql%x{QS4>TlmiI(F2U zI#<}tAOMCtJ-r|rt+wIRkf^nmOm?^ZVT-G##eyDj&JIimU2f7}q;Eyz{}k5cIb1u- zkMh8Se}I?SSKd~HjxV(8{-2gNIYl`k3G=Jz*$uoByD4Lr(}P^Vp4qK_6HrtsD&Zn^ zGZ}WVp!%u#ogm#!q^lXg)C{xf9^T09#0&z6nCeE7^-pOfEV}JUlbc$NBHL9%k99)( zF+ET_Am0UW2nqGLY~-Mayp)mxUJNA2SwD5uc_+|8Rk`D~cF>C?0I34qvU&w&GHfCc zja5fMwo!NdYNtSba~8W#_v!urT|J{ftV0z*IZHM(xG&-6Z-e-I0_c%;6_&VnmJAp&9NlrF)i z2(ReC6&1>A&W}HMn%NrPW|(P}Lg!^|jIZTMf`ZM{ys7ky%$!K@0v~+2#uI{%|Im^> z&kwrLJRKeadAq3gQ;x2kLC%3py*DqX2*FJ3a+nce(*Mx~!|!MbB~%rXt4o-Eiw!QO za{~#%Sp|mv`ZgvE!v`{HDX3ZKr0GomVO#QY8FsrPckO%kKq8KX72Pb2RTYK0nwz0< zC6>FvGgz{!OO|2~-Rh9ft{G0iHs|;VZ!jRM|68Z)?@i3dvzDYzv;@=IjRCI^iI*$I z71mmGyf_)Qv=O3Jcjx%6OlFCB4uO55zPH4z0G2D5J>Vcicm-Ow07O_-#;!s{nie*y zo97vxMEqTSu=j7`cX6IK*#{=pi~mf>q3jxIT~OOO%n^MVv~DmQ9!|pt396lNcpbd; z7Gj?oBbQ2iO{|9Ki|E;5Z#f+0K7hqz`MR(Ix(}+qaWId~L}RIT?Lzq&BQ7giOJ`zs znLvLs(Z<{JST3i)DW(D{aLJ<-rb6HK{Q(NtnBDcYQiL*o9G&K>ke6h?om(b+Q(4wg zoyrpbUgzXl;X&Do$s_mDd>Uw zwd!FC|B8v=Zf7-7tlxvNg9|*kD}*xWo>knTYh6L~_d@u+Rg6)(U|?JFS7Sdw{}Nh) z=%{eR<$C2Ai1Kc#od%{MpO2ccQM~6sx1~SfwSt`#AW`LWw1QUuwB(h0SZPTkgL_qu zM)&oFFriMhdJY)+T4z4<5L}YmV-@j|xiGhbz;vlm)-_lQcRs*wn)hE^*owI=acE;D z@ovWeZ30c+uCnDcDtiJ1&W!}$o0owzIku4hfg)D{C~Wwv5`CuO;yiUt28N&dnYuKE z{RtHTFHE5qCIoe-uc<_Zt&0AlYG@Jb0^e+1#IE*x`@XQMnSEz2jf_b-V@1PU_XM>F znDJwWM4NOF`Uu=k5umg+CdL)j_r(3`R5%{m#C$Y$s3e=gj(AvPOB{iCCId9hdKMAhsG_ohbPHi{ z8~m=Ut!@wWGsGG_;(#S%uvmfDhjDN|p==lK(~yXvLJy8JnlI5&ukjCTkKXgIgZV2% zA_JC73KlWd=5?M*tNILqB!A}pHytc(9uIB0Tu>J{|I7z-xB{YJIP5%DzKGW6&R~fmU5^x zi`ka`c^H7JdI7@I8Ed2uZ4g`!k21ek&>tT|y*-}s?MHjo)#v@qBocjMyhjQn_`NEx zFCQEe>!D&Y_1>)^y_n7t?m-qGg<~LG^OpJh0xjQr22RK+ksSq;>4g@=-(TjpMoG`HLv&`?6-WGS^-N8yBJU=I4ejyab+(jMv_ST>u)&L z77zflLfs+fqN3jZet~j1Ov}+e7x%E>E;W}Fu?`HZ04W_?4{d-ES6FUpC)n=S$4a|X zZ%1tK{NYT(kB$e>atX=zMfyuxLl+*WzE8##+xMycj9I^_QRv_o94tF?;MI^mtCii`K#r&AqktG3({oUC_b1h)5Cw zbZ(yZ#0@R_l@`0WYc%ZF`hosh`+P!KAdX0=o3tx{C$TNj*cBIw&^g7cg)u38lL{<; z9;(B;QUi+nj~5`~#GZrmCr#MXb>Qkl(bixU^(o>W4diJc!GiE2>0MJ84H=*JYJWCE z&k^^#2tEA18v{|IO#EVE1CGHjv?7xW*6U)~Z3@>`omdKT-pAr0UvNLNdC&GHdhX6t!@5^is=Bd%vri96) z0dEVbZzw60Pw!)}12&$eMT$M0P!IK0UOBnf+#@0Jf)K=@dDK#Qb{MN}yDGq*Y*p}- z{cQ??0DtQ6#P5Y+*t-pFHPm|1|F`HNY}->zxe>x4Jhs&=O$5J3LSHMx2}66@tjB*n zwGxp4yn~2#gD7h#R&G%hQ{7jlgO{Vp{sX!z>1}?;#*N`j_^3w+hViaN*3-m&k)x%AbhAN!QmL@UI~w`rNL8hlL`tE z?*+p-qWrmF_WLqXk1E=kAdNM`fx2~1ogZRQ5N~I+y0y2djn?`Zr<&!sCGo%Cm>9!X zE!J0%a<%eFmqL-?rd=n41Vf$ZDVmrFc3i*384EyY6b0@d_^PKCt5G7shY7Bq-WoBP z(IRhFg%pJH(w@Zl-%}Fk@BYy1q^0`lFi^BoheKkv(b)17QFw!lMhE8q21?@p8I!ne z?6j)!s;%0OI|(rbUAN`Kz!AK+i*|x1Zl7Z&q?Oy_T#mB?pcH!;GZg?KePxnft>krO4) z{`${+z5$9`eqjrDt1~jX$QYogynznHy{z~{sR#Ed2r6m;(nn_(_QzCtK+|jbSF)0GL5>6mP+Gn z29ToP8?ItLVi=_&^p8`jaqJjLOl7!TFAbwk5yS6FSqlbJpe(K?VOtJD zR+s#EEWrkXcriH}RTM&Qh&j2T8DlV?I5+YGc~Ta*LBE92u33sgaRXP13^c_goy< zaAQ~V8U}+mc$u_P1$VQF=&_c^&)@!|gs2E$vZ($6se4}(3Sm^-PR)Yl7SE{_B(0YL z{|YfIFRK(dpN5qK7rep**X`T$RzqYj_IiJ7;Pk}W$002j0%SQ=x*}%axTaL}mkp;f zWZ({Nv~j6t8|;c&(qkk*TaR7R*sQJ+mDe-^j>JlcZ0x_Ib^bfyE*Lz(YzngT35cLS zpwSDeNI?u{mGya;QeEqNtm5BTf8lRttUgm$2`;RjpcNY-%D;r{^BB=R>t;f*2}D2g zoifMN&Y1Nz|toxT7i20e`6Y;YkVt!6^WlI(H|i+Vp=??w-QcECF+f1+z+os*&f4?nEb85 z6)FBco*WnsL7N_{=-|AI&BdLzE^^ZFSz@xGBLtm}=8OIHS&FJi`z1w!4tQ>vWD*r= zZOWE8@8H#FjpMw<`Bu<$kJ|R3QK&2j{R(-1um+!3fETjSuiKEjxRs&EPj2{MxlcC= zaHbL*eaXI@Y>PXs1hEv zub?5BQ4(25gKQl-xQ9=>U(K(dulC>~AP|Tb^(077(5* z37wV@0M1hXoevluyu-6==XZ@(LFl5fHoUr0M;Z)%pc=bs^)To(GgZJC9RRPzg3SV~ zJd-!j2}AgKT7$7JiArY@>Nkg6Sp=Q)`#<@tYdvsy)Z{~*C|Lv-v={SEw%BPOB2B{6 zxscJf*o_XQW06CSB$vStq|G?^eZ7!d1Nh33J$(}@AlwG$N)Az~!Q%d+dk581c{W0_ zGNtHHE1&A>yTGxofEUuUYralw#rIxLs`A{fUQ0|wW)JB=K+bobd7ofym5(OkGZ8sU zI^n?hC>$lRTPGh?)M3MML-O*eJ3BCWA5iYc#0b^p_ZZT}dK@Y^HS$PM2-F)a1p|Cu z^{tQo#L!M|SHK2Tfdf@!q=$ew&OtM*+XST>OG#GlH#Svox9G2OCa30CKm7wa!ua5l zMfl^8&a=AiSdfG-#AzOtBb9v+}5FBZw^*c3W@3F{uGHDqQ^aSDeGzyLUlXguX2U}@kXD}h8C zPfanQ#H!RiE^!DfS`89e!E6!;U@;cM!bs5U{qXUYi8}->i=e0AKRz$39*~R@(2AhW zf}(UhFv`U)?-!kll`5RqOz^jzvv^#{Cjx!7zUH&GB`~sky~V6epXkBR4@mh0$S;v< z)u{w}rwf9_%H$uq(8Y70HY~n}>UQ<=(7a>8s`XSB6U$Y6D8!F+L+LV9vcFKUVM}8* zNC-=Vip^Z;-XR=Ody*861VCr;8UX8yPS7gL$zshF`R<;&QxGB4ss}i{KgHLDI}#J6 zHH3Km$HLbFxiYYNHC2vJXWR^-=V2R1jkhbJsC9iTV|p6K#q-$G=UXbe77d43!^4`5 zk<#@6-@l+9V!uA|L2LA_Z08uovdF$7H4MVY(9&fUrEN`wQm zPC87@d8?_M!6XW`8`h`Qv%u|UV61 zqsra|<~3s^9iM(h$`xi6F*dYF+RDNs$VTaHxGJ={WF+`nOSvY z@&_}C2YDJlDeC@Tz8cwsP4beqSl7p`w@j?#uf_UPz@6*=W*EFS!9ibGSeK0~tnJfIsm2R3{?r>_CdUOvH%U1* znpjxO$|I9ErSo9gu<8<*1OrpDpjU)l`7Z85Ipu8E9OmJOqxB9LY0mc0|M5*UU@aiC zeJn4vQ6w9c?m4nf^ACt4tRZr?=t{dZ^PdaTxi8)JP8-1rl&^*v4)&CE`EhETl5)=a zfUt0Ey?;PsLK#P2tMhj52)C{}W~vw2vT`g>b@k4~DT$R7UA~n$Yj@#eMq(6x4F;CS zODgb^ZVc(4Z$tg4t|#ofhtUD-5ZliEq<6dD&26!G9l6k>LBE%cou=wHM^Bn}Uo*Z) zu@8{nYmp7T8#bi?Vobb%=7Rji5jaYA@p=B(PNairSjOAN%N*UZ!Cs=%eprbtG zL06m(sjwt_yCcU-LdI7%*)k{J6r7R3^U5wP#4(|4gbyh(;0}%>eAox5jNf|AoBZco z;$j+H$fiwd`UTHFXIMrv%hhx^G5J=7_pxLe)`g)e?hVzD{p!>-$2lSsR5599dP~|J zE^E2eX06zVzrlsno*tnL?t{j8#djsdYEEYIX| zhF&GH?WmR6X<`V}0?DgLD`865F6^XX_W}Ym1g<#@lQjy9k`a#B!TP*;yaeT&%0IZy;9^DOb42 zFP-1{9x<$n?~N8{+I@^~R2a2LH>K-!=6ZH-@%s7R?~n+_7S&nUkGv+j1WBfg#~b_S zoozTjxl{Kmnm#AfH375MEQ*BYlBS<{QfRAidlE4%XBc2uTOX0cY;H#07J-sKdXYHT zEKEGaY$=3Gw*fj&p5Uv!qtd0#hVMxRRiAl*Ae{^4~@%FMfG+=b#DE5~Sx@#-dZrf56f zZV4}MeCIikqNb66`#?+nPQZvI{?7A1l&%T1%h~=p?;o2K^&M-kYl?>-0zDz{eizhN ze$rrWB`#|RW>h8X|5^hNm?AcGn~-{Rm!bTKvFt=C66@t#I6TB>1XC+Hpd ziu%|7ja52ZV~(sCE)6A-)gk>+3;K8o)we-F@^nrdQ3tXlqX{XE66%4TDr6qgf+@-i z;b+Bbaz0KnXufh(r(H;r=d{UBddkm6AT!XbivV1I8m!U3G%?XSVD@2vTs3VTl5Y6X zIc)zAAK1w9c<&YG;c3cjo)U+i?yC}X!3N)bfM7i+33~kBnzAdikfjEOTFw{73QPK> z(}Mq#LyC8w?PHtC;lHAjii5wW5mswUUF?_bEy?kyndzn#iK%bLL+t zgmt|nDHH5{Xc;ymH?C;o%}EMv1~dMf9*)K|8~T0B2=M;Bk@9g~^2{qH*_Dri3#YyK zT{{5OeiH`r32J%i)Ng>Jbn*ZGF__b$RHj3l40DLG!0U*|be^*D+#1D`wFi=9!c+}Q zj2Zc|*xooHF{PB@g+@fn__<|oi7>deQ~g~-*G4!w1tAPEJL0zxoFSMG*CQMu00P^k zB*IxeCT_U;xuAM2 z?u+>i>|U4upVGwmqoZ(e{MczwWjNhu)~)H+0!LO?0vE!Fb4RhQ*N#8%{tUw#R{P+B z$@qavhQzTf@l#m+W8aDael>y0Nv+K3m^rnEulw{N5U2}^mG-r5rq;TlgXTLl?&xMe zJe!;yJdGN27E45VagxONF0 z`tb)aL`FMfP2bZ!plrd5#5hBcmf7(Lt5O1EC5HW-VtpPrVQ|9((I}~EvTIIpMCT?{ zsYO}HDXil=M(10RvirK;7@8B)crXW`Z9mEWSd!8!ArWC1(5{cZeLSg3!sL0k!V|A` zlBN!5@9uFZJo?HyFzpsOTHXTSSI&;9Ez5!v-j4|Aqn%2A^d9hmzsxWymxyJpIMiV4r(h>cbMC4@_F_f7B z4)oXZfzK7h-}eM0bjS1PtaMT*xpgoLdtw!_@?UX!GOkBetd>IVydDSp-`#mbGPS@8 z4SoQyok1*vY)AgqFoI;D@+LR}Cj;gGwPQVd1X`p$#`p%ugrSM!3xrc=Bcy?B+I))wQdbCG1*+z3&BEPJh9!A;wnmm? zB#sslx?}Gw`t|LylZq#Mie?CpO|^9(db1P+IE-0q8ufM4^OBEgE5FgjYGSnGbXqcc3kM~?RAAZZh0(=6Vx-D3xF}&1&Vo?>f$^O!`a4sk|B1rSR zt^a9f#5zY$Ds`fffXevAvYU&|c-Cn?(7YH5j~Kc!HN^v-5nX)U&RLQ1i;OP1wQcPFY6m~O}^-li^KbrL`DXUDOe+p+g^acsG3eyV9-g1@TX7*vLfo> zHCteyxj(FMq9R|#7?9LK8#EBVFYg1K$@V-DJGR7(>XTc52=iJ%fb+&>VN2{(Cg9Qi zJS&Z=`xtQDLW7dJA#&u8o3e&j1 z{TqYhxSHNXC!EGOEt3~gFZObI)FnO}49ejz&sCS2ir(BC0MITQ_I87DY@E{clQs^1 zh`(1(TQqFcqiYf-oulMv-5szn;dR_zK5Q7TAfC@mb@EtRT)&}r4V6(~c-~ByE1ySk z{;l)YxjP7#&8wxxU!Fo;>z5@ZT#J9LZ|*9Y)m^dPNY%;7K!29qS63p@-1)qd3|9Rs5qV9YJB|1mD4#yixH8!i?OUBU@m#P@{xr#U18=rbfCgsbgcnn zjuhj_4Cl~4{qz^cHyNN0WhUxK<}}f(Jb4dy|Ivw+i?VZ8}i<>BttyxzEA!fWVHI0tNY*r zx(Z~2Bl`7;1aH}yGBTYKY^+A&YCTzP`YIR|{mtfFcba- zwCES!5kmHXP4IPEzdNO&vzZCWG=%lp)wb;eTp8`kn!JfAxc+ri!!u@e{U;aWqKe{$ zjK+v1WVjoW zQC)Fcva78A|3Ju8up7?n1q)lD6tdN&$9QgIcDy+y%b>d$788wSaSRn%lePyu0%zk40fm&g{4;-x!{-qvb6 z>yz`mMP&cX6eQmo*d>uv#-Mv4Vwu_Gs%f*38R||?WEs(-`nwhKZ}=>+>x)oSl%NPH zqj&wStKl`+GNgcgY@KL80>bh&SP&ZBoZw#cOB<1bH>3>evwxy6|J=&}pm@^$Ik8Dy zJ|n>2#B(8|WWph`iYr1Dcp%_(nrXVmqVx~-cD4erS;)qpCy&*r@Yd^tM^E3SXO=c> z8~K+zK#bL=w!`#5YIIWdk3iIe^DkASs|gYp#RwOw4C4-uBn~3Uxr^_cA;!;qxPqU^ zgPw}#xb4O>8&&udrs`f+JbRnRCtn3#G=UJY;3V3MMck3c0_$Cy=cNTm!;w6R6a(X> zrhkb}=4=a2(m5<;BW$6;Ke7V1ls*@i`+e1T8gB|APWd@d=V(~ct>=D+X%RH(%Qj~1 z0;FkWhmdTxh$g6w2XPA_Bu35p1$!Q=@X-E=Ab(0#5ljfqq5T2@(RyzcvH9RjCsHI2 z8#6(KeHg%X%h(X1FVmbynxS;a&MbKO{9?8?rbQC5+c^p$TXY+J|0=KdViJNJaj?A_`N+qn4$nt zvBU#n@#n`V5UfdpqfGf_+h*cugpXeT&v~b6iZ25KGRDTqNL}@zgfdi6q%;cxF~bPC z($exVC*FOmHA53D%)Dv$#A##Ueb0eGP6E8UB`%xIh23A0NLGt548+R^MHLC@oBY{# zENxQhl#E_ezg1mvKwQ zA}fD~zfOSm?aiqS2b9_Fwf=ZQKPH{J1DEM?^_A4$o{5*LSIcix`_QMW{kZObkT6J$k_ zWtI)Iq{;EreSPON1Er>KX}b#W^&w2GhHe4isQeSu^0wymhaTNns>~PEk|xG*Ip^Hl zfpZMXHWzf#`j?`SF@*&iz?*c2QKVP9v~m^^NK^mhVSYKM{Tm0sO$-NdE?=2=Mdt8x zQYca_B4v^`!4wu4(O%_laJc}|DVV_QlDAbSsdp~0C#cO0L#oP3CE09fC4mw97aKlu zH_%4~-g5&b09?O~*cUML!2FHEypk=%hqMegi+l{{%yUY?z(s18`k0`fGTy^T-`aq$ z8~6#lwck;eF=R<(7+ut&^s<<0{89bPPSR7{}073RA&nZOsj z_d?Q)J)#fjrmTk4V{7;4RYjLX7f3uTf-hld7G99EYMuQKKCCo#6p5WLVeT0}9(W3_ zKwji7tOqkF;R=XI1gu#*LXUpZLAP0sckxER>-5%d)RRPEUO>=PFLoBKXv)v>ylq|X zvy5Z6AqexZGDNQC3I>j!{zT7}&)GHI+JI8smE|!wx+>P|C65j*;XGVYbyusmJk$~j z{)7STC$?Q6uBkB9O?*EJG&-_caD9MlA2(bnM42-Bk^{t>RT8- zPcBYNYTS6;CR6qj9)8O*T^dhVlqA$a+uZyxVLLGNbpry&{_~;X~n#;Y3m$Wvxeu*>pEImLC|X&?>r>Ij@(5@&l>nzriB!NWXxtmt<18U9D%KWXTx#18*>yK1 z*pWe8e+5GfBXoppk>`>UOF4%?t+iDKtdehd&tRuSV(rUc3mD z>=`IM(Yn{0sRgUUZlYEZtaYglN4*JG?BX&hDTDx{L1&JF+KKfZp5EW#(F9&bk&Q+4 zS+8dZ;POb*&J*g>f>G;*Aj;QT9=H8}R>c4l+DbB@vQQ$fUTtk-1e1^Q{J1vf?K1qS z@x2)P5(N|p{A7L%sAQTCW}bgAP0IkgmAOk?5#abdsxf|vpaKj=&mLo&HlWWNs?}qU zY`i@75x4!Hy38C27LeP*V2Bx2i;oBzda2(fn?R|x3F!>Yze_k0q5~&c>8VxB0c$La zpnm(}s^_+eJ6{lrzQO`TdYy)u#0_i+u7$UCBSGD9dAe^=rNRk1TGQz~_>B%%Nq!5Q z9-V$8DK)`=(c; z`PJ$*h@8HGm+g#A5#iRl{=J-o8i~Q#23t6?uG|1yECoRJet~owcahLZ+9)8MM8gx2 z+dmwVzn!te>!DT13@b5tL5`?KRsz-BLc0g?ER&%~u^$s){W(LJhPf$F8`wB%N%Z^F zrfh9Qp3&3UazO6-<67;t3fRhv6XyNVN-2Mb zpcUE8ebBU4Lblx@aqzNhn->qN!t-2j(O=duz9Eo^?@HzvZM{1GQu=)qxS;J)igf;)7#HPN+qcY_{cVAIK)#*6i;Z4}wEZ!(~fB`;ZU-(XT zKi?Shvdk}q)f+fScww2M`1O^-SreawS}-+}8AaE+RP8$>Iu5D}tt9u6Yy+(;3oZfS zu6(eR!hcDurIB^C8ai+clE&bEj_XdKK2ETqCnilxwHwjZO7O#zp);HhI9LaBa!fD8 ztXR#)l*?076JDsqRj*%VEx9Np+d%78X+A@C|9`)##Z_zCFDN@n>vafTHSX*nm4Ban zY*HYmY}gU1;m7b!s25i&GwM_B8-3Ogx1yg?JfTpQ=foOLEwBMN-O4ynGGPDC8jt!Y zS;9fe#~Ryd?p_s#2tAM^O|SZs+#P>7rr-j3zfHeM9J@& z2`jaf9npkwTlDVoFo{Ok3G4^NX~oF_8R&ME7KX%b#BnI|skB*nI3V1C@l=|kN4|1t z(Pyq=<4JTES@?8TQKyblOT`toR06*#GK2_u4bx#dJXKkv1r zNy6P(KtqzPtqm1a!%d;WcrqojV4us-y5!qG)A}nMy4;PL0c6b=iQTrD85wOtc~4{x zssF50;HMGP75H^Di@F488}8WGi?uKm+F4De1M5pQJReoCvVC#>V3wVwzLYSaucbir zt;!BcN3&iE0c};2A5+z>&eD^j7F1G@p4PJm%_aOQ4O!E|dQTnWT>wnf{o|$wlA{)| z-s+{*1i1f!tp}B6+COyxeKsZ@M@uCe8KEQ9vs}hRI6xP9$pZ zAW(<&y*}i{ujT*-Iouh0*S#3#tj+~#-X#>cA-s&jj*lmA@4pB3D@bSQ8L0~$?3UYn z8t3bI7!-3Mh{6ihfTcKGItWrnnE9KbEFr6@N_s_V1#w>;ODH&R29Z}cOO$lRVrFUH z%tH(f<~9Y#l*DLt6RUrd*f|d-Z~NPlN2uk<#pv(a#EY4b4>0d?L+*0}7g+vZNjy58 z&QA`4w8Xr-bGmclZVy z;^5GHwG{P=QqUg+-?qSqGaH5$pdur~tPaeRS1KaH?n%HeR; z=5=3ed&H!et6yxUi~lDdHf(W{f%QRR<_sm!rMVXlZ=eFcXv(geW`-l-dR7T117S)VrK1hCjgW+D?*=sJKvwHzU95;tcn`lpLY}enLT71}!-%^rahLMc) z_hvJ>a@>Rc&bzbB=mQNvflW5UAxHuz&US8i;%Hi?hdrcI^bv8kOxNuzRXsr6p}BBd zjf{UmPLi5h5pS(Wq$nB5s%IBQ^K(oJmOj?Yo)$r~91H&z9VyOQh#L-uq%vjoh> zMt;Asme0jprAn|Z*P9F?TZz9##AKf&`ivcXRn2ZuwB+}h%Avi2*0OD9H}c`0E%lqW z=iW)_%|#FwU~&|kD-IsGP&`&im^x4SO;`LbrlMfhozd(9c@nXw^U<}Iho4C;J&ud2 z=sCk21tbRKLwWF7S=qrR`{jbAiT(%x_Gwo;^CvjZvLQwjxT1WY9`BX9q;dNQ#;7Sy z^RXD2IoUp2W#RI_8c@KaFA=U8l0vnGn-_6T?E=RxSO~FevnT|qKo$DzXLpFZ=Z6Nd zhHb~t+?3wM?@d_0wVb@*fOWc49L`bKTST!H1+}jWa1{5k4 z2KtEuRAfky)bp{w1(lMgrc}S-&Om|l>vSa5YMkE4ghaME&B{)q5|RfRE&YI?ge@)1 zrdI#nM&RQr3I55Bj`N0%`2Y2JI8zrb#wg#%pR~mi4_6gVv(61GU)XxqF~6r7=av)B zQSn3mMMU#T6wEGRtc_F2($F8elq+XSAqgKXDNC@<+cgH6BK>H&B?#mo_)sm|KClO( zb16l>lp$X!U+Fo@g|m_MfVltTpVN6c9V<#iW0eQB8@cIC{FnCt8(ay15o!Q@Hk6j< z(mqGBl*SJSNEajdN%1fP^^jycydr%5No@?^7Qc9Tg4jN-MPfePks_E>YSK1S7@p3| z+LO7xV7XSqAO{h5g>|hp5sL2z@^wXYs#?Y0wCJXdQg7tFW8N1-8Oj=KX%EE zT~RdcF||Y?rE?Neicl!)AQ!%x`M01Zq3>ZafI! zk^Lll(iqd*b16{eH?_|3>a-eBtqU}!7Q45d#JrS=c`Wr}nLlDLHgkjHwss5Ws0!&m zIe_8+q7gm3X~uY==Uk~V=!=6``}EMtpn=$5KFcRYr5spA$W^%H)0G8J9lON{L0V7x z47LM^Xs}h-UR9HGuGo#2+hs3|KCj@wLlj(8y8hG@m`mbtT9OqP?8-MiRUBK~hHtca zGjBb(mX}BBMz5rzo~I9PAq{2?d!YY4uM#zB0wqv1lPCi}1n^;x0^MY5_<%!i#(ITe zc=0K3VLRJUb`eCk9?p&GD-wKvviAOIRXPNkx7`z`kkcy5L)Z8A-!mQIKrx*XBoYq_ z!VUgVb%JlmnS>+5kl6EcT#pFyZth1(<2(iyIcdS0(kH@Uj*O-lT+a|Y=WV_fq(V($ z;%F;rQW7?Ewuu=NEvh(pu1OEgM$Q8)cf`iGdCq^6GO2x(NYBw2ic8TW=rQ4+kV%*- ze7Qk$%5!-ww%JlV5O>EcaHtZP`Fv@Kyl%H1I~R;+>ER$^ZBJUmk)fhc>KsCXTXRJa zp{9k%Dm74M{zf*&^obH8o~M?sGF$ZrKv7nA?*ttd>@J5vq$dpL?Jj!DA1`yp%ocGp zmBusSddS94sLvFQw3Fva`fT~cIZo3SbnKDU| zk_pz?IGO_Vwt5`zT}9^CTd$lDGqA5?H&q5?8e#R~Gwyr(Wpai@L1hLWESCB<6hs^X zJYNiru#R#Xy4)w4}D;#KUKror;#gf;AsO1d1=hvWFN(6`& zFhArx4ypEUr))$Ry%IF~K^DpZi(XpxciV#ISy$oCQE7=ZB&QmXp#Z~yDra0K?~o% z8bYAGC0U5hRHR{c-73xkHwPZrjTC zqlQVTPOjAOU-348lRLF4NOK9j0>D+W;;;yi4An~}$d}OwL4E%>(>U)a!acRq9mo5^ z-jif{d=(*3Td~f`;i3hPaVgU|Dt-u3ufDih>ZgthX{A|iV`QY@gxiUfbl!mYbGEt( zSf842MQmL$J+iu2vK03AqfZAbz^jGl`d7v~;Dq4^WkqiSkXkNm)2}kV^)D|++Ko35 z==tNuPE0={t5UP&h|2+z-WpXZOwz56W_01NAl)5GO451h$goHJHeBBI3Pi|x$&qQw zW`>dfwWKJXM#yqQSl$8A0`bTbAVH3cguRLnJql=|A**iX2Y`ZKLk|DbriDY)|fD9H1BvKkO3g2j&_p<|JB4jB>EL%y$ ztF|r4C8-S<5Tx2D(Irv0lA5>vBkVtD`j>8N91#?$(PWE1#ZwO0q)dIC%^VFLjN;b& zGR7G_`bJU&l@vh>EJ2u;A`J7#biJydf!A95&&dNQIHD7gJ;2No%x$YbQ@4nA=r)wB z9kcmEMIO=1Y32{Hsjv1eRoVqQgh9k;%^s z)Bh1(?pR>XNbZFjqi^<8<+*4kB9{&8f#^MVSr|HGoj!CI3c{Af^4xu|-*~T~KIYf= zza8Ur94VOl!XaqEm$oI_Wd#xBlxMAG&zo2xebedj3XPEdu~b38j`#pfM-cOLf7Hl4 zkTJVQWu6GL?e+wdA!rP)p`#n%*yu&?zlfg;-9I0Qs-j`^@moYn4-OPVrc2Y?3DgYP zA;}BOw*_w=sH+kv*h&KVOUxRhk^Xu=CobEqi^4aUK<9lwQeaH{t>P7Kp6vjfE{$=- zS%^4IP`3!tSU=)`N}T+SqRFU}e*u_W_STF_rXj3Nk{-uZ{KIid_Bzg8I@gSPe`X>3 zBB>CkXR*vxy=f04m0Qfpwwkgf8u^T#plm!$-k5Aya-ceFxzQAb9-^H;@Y6x$cB)`osLjE3xl^V#PG?KWJ|kMq6-Pq-3UGIRG%Bf z-C-x2tA%-TU4MPoPKiOuRaa0373nk{ZffcqYlZUivZSbqCYl3Ay5`gW#+YHU2pj;5 z-l*ok8VR>~g4RwU&oqk<+^r~lZ1ps{Lounm3CuZc))@-1U6pppD*}+{G|sfHnGkej zXU%^ISb&9cRR;eLP;M0}G(ajjwTq2wc=yAQo7)d_RPQ6|Ii_6Js}H4zy}p1l{`#7h z+h$T_R+bpkgFH+5aA0tBYE#}NA6t^z%1{YM$PcmAu(;p?xbb4BNVNL)Znz#^$%Bf* zK&7xrDx?hv2N(%m1qnlrJD%SWe%aP%j>Ch#$4yAbBrPW+S2W zXUOk$zAJ5qbfJT?r91=|xaOv)r_Q>{JN~5ykai+je;|BKV-Hakkx7Q$yV1Yu{Ahmp z0c{nq&P(#Jl-&(N-SFc-*35L&I-c=ih)my^5b`z6Tt9T)I>>@?d^|q5j(H7@s_(od z{Yjv&HJA=8_iUOoCkG=T0l4BdF%FZ%%@{zmRkqi08^afU-T;Q}3Jmy^j$8NTc?9Wx z;B*1A4A)Nebv)P0jm%E84#Xn)Xi_VYLr+n~5txMHTa689#@gOK5lM}e?;~kA@eU5i zX6GqoaRVZtj>fd<1oxsdQ;{35pow|BVq9Ii6TfXh8!#3PW7+cOAQpGRg^vyWcww6+ z3|4|U=mz@Y;-zsDpef(iHyY*`GVUH?=5XYObF zayZ=B=Y>|~9y6r9$|Q~h_9)k(X?=4v=5Dd^{GqH`8%Hx^ z-<^?wYnp`~Vbyox5I(NU@-pnd*l@nj50USe{z=i%gC7cEWS~}aO)ANJ`|uHgT+5(_ zpx-R3O!h*wb7erw3E1!UM`ZNwuWN@jAIp7iB<>U9G;tM2It2*yj~iU%a5J1w@53gNO`bK2{b?7O{4_gl{l}f^(KC6wg0vff(dsz{qmL zBOswYRCN95P~@6$h>=c0wzbO+G;|8Y4QCfDNKd#+X8rguG>BRL^*s$f+!zFCY3?{2 zDTlKc-=FH&v+AdebSAj-jzs_&YI)H4J2xBGc?BG=(gTlhVXtVKhW&XX(8PoD46 z%8yQzRY=Efe8Ua(ULvwXJ&)G`X_bjlx-u<7Lh+|K>Qt8=i!6@L2g?m9YAXNO88%gz zWcZ(g90CE`{>Mt1$mLZ!i)BR-u|AZS7QpVsv_gzwTe)!uCdtjT2f|C(BL-oizjlbn zV{8t}K0W|yM%6z`5V*T!_V-^Sua_o=@hv1iJ(R_{7{|bzXeK2ZsE0Qe4v=;4A|#lXwu?}u;v!qq`C$s>_Ou5M#dg z4rG1RJ*lWS=q>p}^Ojhaz<{v&bP2ju()wCqyS+zR3j*217smsTXJncBn6u7vF^~Lj zbxYAE6j0*j$cdJb07Mr-OcHvobfkeZegUCfyFEjCI<*F8z5}M|c2!?)MKBaaiU`*P z-m|;D7{`Clh+x4hpfUeqJFT_*9`4H{*?DsI7T*Akq_1MRzr_Zc2)c(2!u<4uUMk?? zsYn+}qQD{Shsw;8k93+j)E-;Z(~l~S1H=dB6QpBj8k?Zlpv%ctnc-5MIDf-+o<;pO z`BQML7hL|z$a9A%li>)}$bce9q_P;Z(6Tj@_&iIV9-?OTwgye8Op(+jMCL7=ur>m0j1oPJo}DsMG!`}kbAlzR$a3dw1 zF8wtLE8*TbAGgNWCP;32jgZuOSfnW}HqPH{+hu!UEAo%>QtSq+8NxwAwCqk+-LjBG zA0S^^P>A7GuFuSDD5uOCbLXuWEvb~I0XIMiu9Fz--Ub;2P>`|-pFqKr?AduLFa04K z5*mG<5_bK_htDTnPG^lQ(>45I@N#3%nw5htZOjPjU0(j0H@O09xO29hBoE6H{t=5v zG1l79XX9)1_XiNUf!nrK=Vl>$I~^I18`KjN@lTZwhHCTftlGndT`(|@yHMCwJ5EMl z9si+*YUTfW*%>4YsN{D|CF^;YZ3hHM>}5uEcWW_SAg*h|+MC_&%aB(v)!8RfU~a>a z*C4Yt>?I8mot1s1CH>4NADjU#j75mF+OfC0#f9If@P}!^gp#&a{t+yE-8 zT1^>^PG-=>q2=yry*Z+wV-7}WHwt8RKbD@}n|D&dQLanz{X6pt*y`fi{F?1;;imwM z#{0D6-X%vVOUNZOkuZjMCJNjsQ9FbFXSn+J+Q8QRY(#Knjy$T|?XV)QZm98W zWm$#(55lj`k)M%E=q`uu=Gq|EE<7;*=iFpVXa_x#J@Vn@edPz!`{V})02f#5d?0Fm z2RLdPG~bt8kXmz(xM7;P7ugxM=02DJBoFPTTFlYt{Jj;qKZqOvHl~$^{GrtK6-|&Y zycLuz+*7cQMUX#As2I(%X)-dEHH60yR+Rx|99RRQflvI@hHLgvP+GYp5SS%0+OJL} z5za3qj1nb(5;Yysv7;A8A37y*iMlGxGJ^^azj*J^=!^kK67#%+7S~-?9b%|KG9-7Z z3xDR9IwoVV`lve5JRMDkrEHADKR*rC<72^&6nvZ~KU+cnFz8C+7j^-_H~PX7#rlYMFno7hcMHPR ztb##3PDXB7Ck|W#UuF|h%XRZj)K1@Z`4fuvX)+HCXQ?Y}h;EGeh4##1F`4UA95v6c zN6h9J3o+>CttSU}^y#x}7*pfLKSfc%qt8i4d`Em89~CG?v>G<>!JDu6oO7``$@oS& zlOz+1khT@RNY&Fb5l+0+LX)z&H;={x`V^FsvtQp zO#BlK`_(<-*GPI6$nKL(5*nM4JW;BTRvJrF?Vl|s3-!5Z->SsQ15x(&?u+s@<)Z_q z#?x9kKxE{C`TC;+(RT%yH0_!tMEI}&*2Qq;p61!&p@z(RZdcsS#c{dh?8D=0a zifaw?gFCVM(TAk6X^wd#H;GzANJ1SW#>7V~e^Q1^zTrImt55s{PXrkp!jB1_eu)i` zZag~Q)NDfG%upq2txca-?UT%`tPlh!mf9Xu6{UriL+ahTBT6zZ) z%<-m$0-;QD)$gmC3=vfAQku?1`rH>GQ-27fF_qSBb%CJS|MzP}86S3>%6FDXsg`uK z(RNt+h3y-c5D82-aEoNJlBfxj{{NyY4A%~dRSGI3-a6=UQhN)(V&^4NgUfnvKS?5} zbO#IrD=J{co0a{|w>1Ln;A9=r?2d=7PYOoVs$>Y80$k}R$hbJI{P@8jf4~X02YX&n z*>L^YVF>LN$R!4a$LxBC)g0Nssv0>3plqa!b!M! zt#5N>=;9EQvSp?6^V~e4_@4y)2jrmG67JCb>luK%`-C`$dkMEOv}+$P3;t~rUUt#Y z%RL#4EjHdaH)RZj6zXa&j6k?aIIn|kA?v)A*5~9o@=Xa~+M>=gMYJ=8Jb8Q(*rd39S zYeSyf{GF&w94#vI(Cz5Rj>QkoE*w92E|Ve!m?nepfGSPSF*XppvfB|1@VocWWJU)s pGPZ@61hS8xgix&1=Vr@TcWJ|~F6YfZp;(%l<}bhX1O1gcc-GL3qFev~ literal 0 HcmV?d00001 diff --git a/constantine/bls12_381/src/tests/g2_compressed_valid_test_vectors.dat b/constantine/bls12_381/src/tests/g2_compressed_valid_test_vectors.dat new file mode 100644 index 0000000000000000000000000000000000000000..a40bbe251d90e576060adb7eaa2456197878804c GIT binary patch literal 96000 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 literal 0 HcmV?d00001 diff --git a/constantine/bls12_381/src/tests/g2_uncompressed_valid_test_vectors.dat b/constantine/bls12_381/src/tests/g2_uncompressed_valid_test_vectors.dat new file mode 100644 index 0000000000000000000000000000000000000000..92e4bc528e8937a311b502e4940e5e363a1f7c57 GIT binary patch literal 192000 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�uc*gWwz@d=S2~bA~LrFNXzUL(>hGG#V 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%0DJH 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�~o*k8y~kPRf%r=r;Zzvx~~w(z~z%pziP=uwF03YF?}G)s}UZv>b8 zk=4G%ywS%X%o7T&T{c8cNAmsaG&K(y;h6m;R)OJGWeXi1Vj8~iYv_{MlZFu zT1IoyR;Jc5#Uq0~L(UpzpoOdmwlFkYQ6c6a40o>?!#1&X7!l|v;mJ%=qT2{;n`C<< zh69gU=Sz;TD}RvsAy@DbE=?qK)ExhhQAYGWD+|{te%#im5l*(%fyn?O8jI%GFFy=D z&jz#TPHW8OmbKjkBY|Z5O+o~xNtA;7UOvqEDq2~j1oP77G9&f&NiIe25A)e}u_zYD z)#Oc#N*Bc$g0QD)ftbW_h*UYV3Il z$d^qOuE(vz3fxuJXi&h5<^)61*__XK*&{c`6b(1i-YW9g*YfO)NAeiSC022V=cHyA zDF00Bh$<6&cAfyDgnD_qehDB;{2ki8Jjj=yz?UT@0BZ)D?Y*48C4e4bViZ~cE8JR9 zOQj>x4La`^m1K%Xiu%yIm=~^v)^U=96Z@c~Q(y?_v#Sz1cFlihf9w1M>m~TK-LTfSGYMzrvj*lyv3n0C4S3b@Ti*IA}Sxp3Hwa|j16QXk!SJYZxe$}Yh9lMWU%NPbM4C>%p3{XMj1^=QZ z1JuSNaTwM<#nf#9=W<#c9Uiz83RebdHkW0T5zA@Alc89Q2w_beoys_FPs{bc%PsW+ z?I=iwdx>vRlnhgU1I!i~=~#FqSBK87l>An8)Go7{+0UnbCqdvm%>jO=p`>7$}}Xme0=Y9OjJemuQEmgXpbnimDF(Ed~Odp$R>Z>Pp4KXwbFGi zd{D#OWGVy|0su_4X$g8DkC?K9gpK{+6^E8tC6iD2#t_K@NWfc9Y&Y+!_$i=w0iAX? ziuR@rM9)8#f>_E*0d_y8Rus&cHhuwSlr&CIlEmnRF5Gq*25S_??e9vxO*uCtUtdKR zavF%F50v?baUjX_>1fX`N1`6i_k4w!+)zELg}^#Kg)-b((BYvwd>Y1RS+u4VqH7bLPKFp3v6Zt+iCn*&Mhw$ z{{!x`aK^x{`q}@ap@i6dp0le&nH<*xd7c72#CkHNn3M_5kt=OXI9U<( zBh(ICTAxrxjR;pfQxrN5LbV(W#_jf+9z&lXa)2p#5D6S_vf!ryqsfJ5RT#@&6C)38 z@3#3iZ`{SVwuS#|f&&8-bp*CAOUV1S=hAFwB3eK!Hswv)BqO`{O@aztgbD-F7v1|_ zs7J>+x?PyIQV*B{YAVh#8DWwR?d2u^B_Oq2r+yTl6gZUYP950E-HyNQ?!%o|_VD4m zpge#5z|9I16D|>niM*f%u9Vi|l92jHENtYt=hTV{?Dk?uI8W$p(qO5~J8Fe}DfF{M z?YwgWl*;{^ucMm8Pt(yt!n5JQdZjKi;Dmu{dP)?k_IWEGk=rCIen^f`7$FzCGdr~n zwzW0v)zV<**~aF|of4}Jtt(VtZq})6zanGP;MnkAGuzz*PiUG)N1X|29>D_6T~CH*%%D=3R!e=osVbp`H;D^2prs4`MU( zS2RRQgp75B{=O%L(?*DMC`kV6|I0T(?V;8a@yO70rO-$vg24b8drrotR$KofVf8uC zx(ofpB(AD5gVnxr5z7-GeY~XkyM2SWmT9seFx~$m$uP=3xs-9 zU90nCg4jv2h9423UH^T_rlsNP0<>18;sRn-xLSt-kuT_$RyARK52_y^G+%X?WcnY0 z11p!!qbX4mm=z`C?vElTx8Rrob{=~(HagV+6~BOb8sPP75Om&g66_8YddKc6aJWoB z*Y+4^*$N_}YBm^i0N^g!#zMJYf90eGMYm%hJBnG~)iLrF`EE7e3my{-M0Dac>xYvG zf{Ie(b-L*6xMO+NCU>$5z|>z0wjei&*rx5KB-lia$cjoYN835op^H>cpa89amBSQf zH{!pQ9eSCcaX{C1AfPV|WM{D=#}8P)f#Ml*4jo^hxMLPLAXLXC7137;FlSM*DsXF- z#%Lk+I*!(@hqQ1F@Z?S`Muo57l=1)Kb@~Tj)=uPDnJ$7#s-aHNYoX-eg`w9;Z2^oM zVi_5$KT`k~H;`J%XbXT>Z(TtoFEv4t*71l%`q?9m%rV>myh5NP3FUjNJ6W!M)m?{! zyaUVwULMfI^6pCRM_yD`8!MwpKd1=#oyoim%L%vE=G9=_@EYwYz$ap`d`Ht3`G+pBvkfEoB1A$H<`Fk)j`-aE zoO+H3+{$REZU&WRK_cQl@)#O8+u*SzB2HwQd@M56B%=bqF^Vb-H1X2`w*MFTSd^y`DN zOBxN2SS95mA-&au;*hk?SS}c=&dXlb4iazguZvO$Y*h)z33E{#@tgXvs6vK}YSyz< zT-Vpi0q#Iq?Re^{3Z9Xtd;2TCJ)HY{*K@V6occb)zB$b$3ra%QnPWMnG33d6(lDNcIavV9}(v{;CJq;lB5D+=RbVvmTf5v$l;Iea0CN z59*MRe-XJTsK@B^4#b2ELVbwe1-%|+L0sutGQ0iuj4o9;h`XQ<#6_lSYk(;ucqbN# zY`H-cyS|(=GG@mP`{1>dM{b4(o{78+?|+PTo9D)7qa9~7yAry`WYW%XTUp1UTxQSK zMXK4g9UN=`kSs{NCkeg=m*6z=6Mhg0KZe{)0{zO9YZ|fDQ;Th{m~${(JmX}d_&W&Y zY3tAgnBlL(c!fTnKcE@GD+INP&Z>PsZ#A zU@#~ZIluTz){{G&>Bu6%3>q*0K$xBPD@3gSg;Hk?>Z%WSYnrr@VL-xS=qsBTlVM$L zLkF*MQNTlMV6oS)Px!a-Nix$t5eIHrTvgYdgq|3VYFuo9lj;6)6J{9~@>aA*pU>lX zr9(Qmi}-)z886-_s(t#H0DqgQk^H#e@Z2P;+#VfIj%}OWf~)5zNp6VYs6mscgLEQ zy!Pe_e;@m1%R`UD!4v1G50)m&XChU1e~$bBO7*&vs2~1zrRWcfAnVlcM(!a|DNvVT zB%HeK$6WsR=m<8*a)T8NNzchhJeRDVhPx6{<+Mf@ z0Nm}DT-64S`Hlprc-A^Q2A3ZPfF3bg?JfwUD*C-6XSb_L2KlD&dpIRi8541ZJi=%( zE^@5;^&wx3-)V%$M>=p4V>4kN%oVfYIu@oaQse7}C{hwm>$Z}EbV+!*33b|!(ojvI z_8K{=#J!o8fVOuAx}3MFhMowvl9;jzm6u?gA!ZS}H~DC0f!$>z1vyzJW#9dc_h{x= z5JYfZOAFEvxVXu2s>FxC&g%!b96dl2xPjL^v=8+RI^Hc3B5j|3pYV4dA9$*0$ zAhS#2@HT=)X|{krw+n^zvr&Q=Jvqpw^TTW6*Wx}!+*b6dpGLNv#X@r|o5|4$(y$wo z2Tl1g{485+_V*GF$LeZlXc!U$*h|>e*FqoOKZvNKh2iO{ zCGFxOI0$l_SdwH|@UC2<%>iSqgC|>0Nj3u)rKYqSKfzPeBk^>35y}n#)ZDd30Ay*m`_Y$!wg1UlMRrTtwx00e86yq$MD@t1)XQC*m&!|S@ z#^kE9-t?D>l6sVP|MGYgCb2Ye({hu5PbbFs^Abz9q$2yg>%5Xwn_ADMBhQvTtV%zp z@wqM*GC_-YDIabR0*mAVFMZ~j9_%{`kW`*pmz5s;s3roc!nlJRTcoC^219q!Hi)~y z=(1Xx#vud-c^%R!f@j8q=awAfFrb}E(dxsVOV)vQu@zX^_VR1S{tW0ZVi!Irr)9p_ zYRw%Frd6QbORxIsfve9*g{1ag)DHyeD*w~Ec+Pf3~S0blJ?HP3!5KEG=h6M{?m*)_=MfExe;~V{~ z0dj$|w`vPW*!O?U_%RCLwFCkr=c7aVRP+4|$9xZ*AF97FY-tkwdU~z$d-Y1e0_N?e zFRc0Tn58``n@Rh2xKn*M!T6+x58tx{V*3wGZ=)OR#o*?@p>}4#-q;F`RCgQ!o&^-hyV zX%M~_rL;R8$cC%o^ZGfeqGg0JG5&BA7D#b(KE1@BBYH^D3RK>mPIq?#US~uhq(lP( zwk@NkSD#M@z@7c7Q7iT{%?DEfvo59Yz1sA`c2#>|4!AOK1j3HO<~jN5fc z-qk;eA+w9mrb+G#%HMlq5Tv~rOg!H3#+TE1DbJ~m{>r0u9C9ZG@*I7Rb*-seR(i>2 zYe8bAqjovba;Ldbl;7(4J;HnPVBc0e)-xl4&FAM%8H9fmzHWfmtR1LLyjOU zagakAltsoJlw8XT&epB^67@=zmhw1c?^+x1k}!sWIPVZl zv2lZ1D;~3Zdiy>E4)N8{EYq5~nZJ?Y^8?-K(kbv|6l*K0Cf#tjua19amBHMSQUp}a1_GBP801D=;9XAgU*tf%>mI~ms zu%brw95`zs4G5F|0?L{JX8=M#y}xvfBpPuThuCy;k6N~+;&lQ{Ngg2L|K5XJgPvc3 zf-p6DSORyPpI2>kwMF4u7F!Ukc7W5IO8~)hgBgg36PM(}*0V8){;?41TA^o zktNXzTv37;VyD5g7iMnl_?bLv^SDhDhG9L|z=Ac|$r^OyzXQP{CUKR~rTu||a9#qx zh1zf2TDx%s5-7+cc=l7nV4bSE3D#K#NGf+luiUKqC0y0`{(4YR(s}KB2J732DlsDb zflzeU5^VGd&2VUX4D0%b|0z=?%ee@^Gc&DX`D2xx3$u&Auv@1lNp@~yp_y5?OSGH| z6BBG+mG5EpOEBopp+zrP)pPBBHEjBL#97~{>p{p6dG)9hbhAeU2AE6CF3)uG1~j12 zMDYMx-cTIwzgeundw}ni^+`8LhSO#^fVa7W(!WW9eIUZjS-85bMQ{uL{-S^bV12j1m^F#cZPlDqiG0#r7Z>P5^pQhTSm zo919$V?DJ_A~g*0V+h!iI>+Mt?}p}Ius}27VyW&bv3{ey5jCw-Cdw>t;a>oHM8_nV zpExzc+#U|M(IT^IdSFfSOWE~@i;<)RcUd)f?O169#OJS@2ACmpHa2jIWedj=cgyjj93q9T~1 z>~4&+C~D-?>Rn~b>wV>m4V?m#7MCH9J>}I<+7{#&QkK=f@4wIh)9*x>;o_)o*M(a< z$qpA0RwjeWcIh=g=B9rP5PoDuA(Q{!cd?{ZFnRAP7eWcw9;nb2dMc*j!&^dbtW? z+SK2|0DMN3j=4xCcJmfqxX3xbV!Z?p6Cgi0V&mG`m2l{Y?)3w z0ug=3`(wGt;I};vRE^-i*KqCunew#~8hh^O>4I*1v+ufvA7I*HHcZ(q<#(`{6fOjr zf3`gFl+V>g@qV%VxnCV~M47`clahMjw6 zlR_c!UscSclmC;48-)G2c)92O!;zk2CUafL(-;MS6pbcrdRBm*5q#-vPhxa&U=Q}G zO!nyvO%6{I?v8w`xu7LR)f8@(onO^T#Ax-kBe6Y+G0 zO@_Z@#wIM_fsB#wB|VK&ZnN~u>(uF>!ykw72n$3QHn0o6sy)A`iC4ukPd7a!4i1q( zG`?GU&TAYN;v7%T4zs1j^l!oO?pbZ(01*iQ?aGPC#Pr^?9SVnRR(VUN2RFUaGEr6n zmUIfO1+G+_66M8FO@gIx?jQ?74*7_-%g~c(jsy0Lf3ZNHb z364iuprdxCE@k2ET>`APQ60fPgAYP26}zbCv})}6)6M%#usg}JX|=-h@^)c@16rop zk!$`AL{fXr(^3Gh|6vk9o(spFS+Gn(=WV0nK=F1N%nYD76cA6Q{1}L85##aV0OGiO z+@)|1+F%^h_9?h7h?2pg32l?>w=&zIQnA*aS1V-j0l*P2Fc0j+5 zogLICIZ2xor^JGxlcDD`qR7Bi#l?+lb%CPMJEf+10P6zp} z1Oh;C;3xR?gHwTEzqj&mwFW@64!(mF2r9gs6Tu0f;e_}Ov{wk7_e4zsE;7$2a}vl! z>%|>Bx3z#82@Ga zOvv=!Z;9wj@N!iFU=J;y>6TVa4NxG>`M}Q^xD+h#w-FO?(CBbE)}@LMvwOO1h@|iQnJ*_)h+#`y$hcLBe6)PtfNj0RBS9$L(=-Jk8IV30WI(paAFf zPkp_&Ay~tU1zwj&*W0N2WsXmwzYnYvfVLmAf4Z!S$@9|CDkD!o5bX;Oe1wnMRE#@D zj~fFO#N&{(*p99Wfx{uDJe4K!qJ28B$+$ar;x2tVa|a_z5=44EZnQG%&eTwfr^uWR z8j#sa=_xjileoYhvwh zQY4()nxoLFwf?78Nb+Qrv<&*lXE?i@7+ix6wyaVA6R8<^(YnTH_*a$IAJWCFDT+Dg z&ci>h*r(QbNDs9FE^!=*xw6dT5W7h84N^-}7@({IMYL@M-PBoSt*I?~qzZ<6F&_>@ z%s_)xoPB?4=r(P+5+1KY6H&u?p4=Kub2uCh78ksw&lK?9`XS>dql`X>mI{0efVeO=7(6O*{b*oI+A#iFO!Ji=olpT!rwF1j`)+ zaV`~jKRUei7nuv2bRMt%P`g~KbXQ1r+U_hE>H}1hx~Q*VQd>Pf0shL1b* z-18X~2o^F&g0|W%T7qITD<>TiiT7g{wqwTO=v)Musii{NCVQ{tDyC|VJxb(Y*i8ys&WE4_3b;cW~idURk!bD}m7qD7V15;7DPtheeUe>uZ zkuVOdqRHy@wX9q-ZDm2x7ev;A7_;NnoRX~Q(j8WK`NTzV7No($d&EUGnYHpwMw~nZ zT8PS$KG3RpbDKYIh-^~sWl;a^F-{^a@|=a z=ww|Wn_y4DX+})Kg~l5MrxIp=^t#zd=*4rua|TYTNr_@sbW=F+)? zU;E)>3ZxEC>P>S;kV$T@5ZHp|33$@3By zW$A{Zj+W0Fp0@k)56+Rr*W3P|PJ~@3AxT;K%bve@Zq*~UW6`#{;J22DMV>=bwWgfI zGrQv^zyy-567?l~Sqnj0^J8kwrvOP z?ZzNO4}nKoR;cab9TBWGHBB$|yMqb5QwDjzN*j3UM76Q^zln_L2T) z+Ei1%D71FSVX>6FVFA1r<2_i&9q_%a46o#OU(-Cg8wY|U4w`XH z1+&5TlX9t=I;gCoA0-i9Jn+5O(>hU*6>jOF6dU9piajg+WoA&R?6@Ry{?lVO|F=gh z+d|nP$0*>7I}4lg`F|)+$m5nl)gGca21&cG<75Up%OCY?jY4_gF{dHD3T_U{mO49;Zt9ZX z8L;#5k!xhD22sB_8jze1X-#mx6eo}_2S_;xSXXpHIbgPtFRJv{lisE0lv<(If0db5 z6a{rzG$TaBp~BN9zm8f@P*wP&3{F)NBBYY$x4U42#nCR?(EmS#g=;M`LRzMYpQ*#J zzYR}->YX}WY~4O$L0yYp2P2gg=$E2dgD*7vSrQ0!TAh0y4HK&q-t!*V1zCp>g+;G5RLv;9XY=H=C`fLCCROQz_8M zU>pZyhG{9A7NWSL8z)MuKz(;sB!Zm^U|ywZ3>R2}c%lmsalH**r$@0k4c?54P%Wc; zzx0xU3Bg7;{<{@QTQt?GbTR>_|gaw{Q*4E^{bmF z61e;1sS;fujax1+{`9Xl$$i=sr`8PNFI;Oj7`GdJ`|0}MnJJTV#CZ$av33Yl36~IC zFDmUfre@VlKe?Yu))Np!nZ739X^kBJ!|^l=&%!A4wx3(Ga(;pK4kiw_3`Y`INcn;& zMP>z2=U+}u>mIye$qo5341t@hL!5`#xUfVd?w@JYOdJ~{`jfh(0@pPLPElQYFjJgg zF68&kK{iq_F(d&el~pR^`UO(ATW1vv!Vp}uUpnKfyxR`V251$!ebXB|H_ zj=xDY6tL^KOqV@AJJ34+W{=dJ0>B324+deIj)Z9p)1bPow-@`P?2ijHuji&f6SndYc#M@_AXloY zdGo}X8J%wm>65_w~3iu4}-{~OvTKA-Sg?PDhTt1 z35tE`ln$c}q$*@(lf|yZC)SaQO&&EZU!MGP9pe6P0}&p_TKmpoQF$Kgz%6g5l<1wQ z7CGDW&WKh(M_}b(CenOqsNG2bZ3sOE8)4I23OPmQD=9&%w3C1qRvqGXq$Tt70!)<; z2;j>Q-co1X5f21?T^zY(CrIGF%a;Ce8MEM6s7j!*v5f-0Ru5yy!id;2ny`i2ytv~VfaR00DSA@?H890fV%H;-OrKh9g2+Rpq7{h79sOz-XDPz#1j^U zttvrfF7}7U?}8?s2uSx_wAC~Uyp9b;W&;v+JU(58jgd*MXHcY5I^ypfyb3LSJUa}D zn3Utz-dO187_*T^cL-0u1`nu6L-ut*F%Yv4S=Rs(Y4y?)x;AWiy%2wBbxX?MDfXU< zsGE?a3YB?w?$LbSwr#v(bPIDeMthD51nvz}vXi-VA>Kf!`nij$;2ZzsOGMRQ9LI)_ z37iZe1He8109!qzsWU+7K%dHHbGoz=GTGP5r6}Jn!?Yxkb37E6(G+Ojsp_bSxxr2!E9&10Q?qhaKjy52$h_RLvX)Y zaog81m@)Eek6Kvh{{MKei48|3Xsv8Py|V2ih|5b>LjXph>WBB)2R?&it$x;qoDqr< zMbyqmH13}k4C_n3y5r3orb0EFI+72`E!i0|kW;i{Gr1R06?9*4{2ACqE{fkky>@^c z(JA8z@`37(>GdD9>X8BAu!5C|`m5 z$~TZ7GLavE*)Gl?Ox0qGt*3+)ddI{B1F`5HmOVX~=j7mhL-t7}>)DaWJE2#2@dhQ{ zs6@h>=Q;ogy%B6`r9VH~2+dYy6mSTAYX!$L;iW+>Bn9PSezYV3bXZ8Zos>B*vtYfp z@14{3>*!2qD;tTD^DoRm6U+Tng4wy%9*-UG;?ILAtVblGcUM<1%}x{^$<(=!C(dm+ zufa>WeN)$TIxjI{3ernMp;d#9OfjvieSuQ7$2a!vMAA-*q%Yad{%r_DPgKTu&HGB! z!wz2>+5m6m5;#k2`6<|Pm<5jH^>q1D-lBn8MaK$wCNZo@Bi8K04@soiR8r9oCfU*| zo%|7819`L@I(_)6Ej4ixdA``Q=63YIN%fgXA*Bf+LHJaou9zUsJ_z?|QB!nx>cptN!bUC6{|HIL6CGsJ@jd@UeAq+2Z_Jc%U8l*zn?_4m4zJXs z=K;X-UC2)EIzH=Z@kZ4jMIxr1z*X|=Q9*Y|{hs zZI^b5qhRtN8Fe4B1jM%J`eCkc*S+X-Jj2JGJ5PEP-iOI3>2gV)5X84x!JHKHo84S3 zL;v>mW>ApzUksKI^G0H*tIZ$7HF*tGZ=Cx=#Falqw^-1{1CXsgWq^@|>IM1fyczIDi`lQX5Qy6$jL&-rUqoaneM^f=_m+4}%uAsQFQ2#hI}Aj}EymA!`?o zTTB6%$x&%E`;`@sR9esxh;#;<_ZM^G$flh(nE3k!&khWz#4S*tsmrj-oO8T=61}@fxj4rQo^Z z6j|0owbul{7L>eBlIR$F=h#S;N?tlQIQ^=+^Yv!|n@q*cWq^saWR*jA8FH*C#ocCf zrkn}@2>gl8!bVtZCiA%mZ-T?Ez99i6T1x0 zd8hk>wSa?6!{w#D!7)$1k9{eSqFkncH=qG9(~y2H64HnW>95--MB6fj8RkPausEc+ znAj;t^`av1@1D|~L?|@+pgG_7eac7Su4w7#)dy5jQPJlE@X#kB4S3~>*#TT{=VWxR4(3gN8b0^v2PRJP7_6aBZJ5|a?AAZ{`x8vm&ko3%`+ zY=*y1(J~1+Mjrwj^Mi4#F*vozYj4TZT9#iyB85Gz3+I1|B5X#VONu8yxoM|lDGSQb zcDa1PJzO?W1$?&C>~$zFT~|;YwGKK|*wV~(6fyOY_PW1t5z)Daz@zs%Fs!h8=W8e^ zv{LZfKA?BaS;j8gL5}~0OAk^&+&KF16Y_(8cKdT*aGd?!+ui^14d!10ZU~&Q9Q})r zA=+g6!FP&Q`4NprpXE}ei_8X21%NdZN66cQ-P7Qr5yC^U#~(S^`XhH+!nNoBjd$g} zp66_i*KCt3-IO~Vsbe#U7HD!*=39fTfd{Xc?Nd#9=(QNI_%GVeaLADOXQgGsI~$Z$ zL+01Eyv98TApdIH6A!9ruotvlNHMgjYyh>Xjqb&!w&%_MCi^4lPFV$rUtFa$%|*g9 z(oID0dVr;^1y+3^BjpEf3Q1@tA6#U1i}4w>RwT~jA+j2BdWXjBgY3oAU-F>{T#tCo z!38W=32_}=iy}OM&gCPP|NC#P<}OyT*WIwM*Co^+6`t1H-xEZ+T-Mh1nLs&a4Mv@b z4-tDbB`9EQm*A~6geA_ECzTm{FZih%ZY^Sx?qr{Y6j6Pz!ka_Qu5wjNjL+P*0cK?_ zjQl>27rkjMRSNcT^&AxuYL|Bn%IYC;oppkRVqhR06jA3%z` z(;pzI0(EKwR_R_}$G9}0&jr!I3$k(H2ew|ZHAL&IY&F{*MvVD7cN)<+8_=}bE?HVt zaT&+m^qBtV4+#$v>L2}rbAB=E2ad}Y^P+GzGzzv7`JonaMuI4t;LwAFVjEkiMUxWo z>BLV_TQ&}{+|!-Sxl84p5|y}!6@v_RKbMywV^O7OtfvtogJfx4a-bddcD4SP*Lf7& zGSw^~DIqe0AY4602mjsgF>={>T@_PY5sZQXV}@l#1-+#|BRA&Zaj33nFON~|ViU#z zt5!XLZdtdM3<`uhaf?V^Mzyv1iu*O&v`}lV3U2c{*q`+T5A_2>L(*~NWVd)SXg~S~ zQIe|e1j|(DuThB+>+7Jj5Rvm<*R9~jNm!=h)jV{GTq=@jBGo4&VfF*)~;h>zOpuc>OZFZyD?m>--r01b@p z3j)H)l}QNR_0HjCt}zQ^0iBofYbDQ^Y>FkPUi4wQ-Le_dM;W>K9qT$w1G;u6kZImM zO(2PU*cpDIU~RsI(rg}WttMZ(XA*}r&~UclhvL`DX1?Z=CFpgn4zE3Qewfc{Jsin* z`S(NT$hub?EIjSx(fC-3jDQec*~FLOL`R^UehX|e%K6_gPKREg-KeV#!(4^~Z&2ONvg{^r=bwugfgEfU;MFe7iA zab$1R_U}eN8+cM+Xg^g`S<)quC(V`=bAz9H40N%47H|XD$K=ienx17(&6@pr2zLS6 zh|z)N+_EQ&a6OM6_?W?JR84TQmx|_)1r{mZ8Dlh3K-7m++!2__@ZL?^SGy#FJmMZD zHGxQY+|UD3YFWr_&`>G6)AgCk0Nr$dWitx&N;w#L5*~4>+uE&Nnda}2u zt3hz%2&kmdLslg*rF?4a6Kd3>tCwu0jihN^h~VXS&wE-{tbX`6X6Jw;QXM)W`7x} zrrvHK?EeBj|0G9Vm3m7LnUU8-0?kh32?w^nA4TpV5#$;e3)N4j_>FR(IZlbf`c9ie ziZxx%r~UTUl2BO1C2N~zSg@52TGiqlQ;~o#fgQ?j)nvLiVcS0lE)A=$o26=!dErHlM%=Ags-V zMqx;X2}rmfmgMQM*heyNR9^bC0*J(EVez3X)z^Zv9bOYpoS*Bq$(KR%yR`^IXTyC} z2Id_92+V7}CP>qbK_7Vt8JJXqfsISJRmV^#<2Rga)cQn5%u=%=a2g|;yykLi6_GGC zD)s6z-LE!$*=3vLLD>aG07s1LbOdOmH37^qAsWf*gF4qTG09-{0 z3%!8SoF3V@A!kxV{V(GW=;3ZI?yQPT8CccNDkH7`GWMH_3`m~)-_#s|P7`HoRF!`MR2`)RBQQCH zRtMO{(13~%D>-GGXa?r)4zUt3xeBMkn;@l*Rpw6c{9+92i;{bCPZoAyDH3BOeOil8 znI_>@!0gI8ue{f(7OJo+Tsxm=p|!Zk*1*e*b&0atZFja;W`D_4Hj%>Bl=c(LVn9(w zdQwW*Hn@EN4B=9tD(MhiQ_E-eo76UUN3gdA3Po7q3 zG43Zd=I=j#db8R-L2+yec$-HZ5ajlJt1Z-_(45Z4shf!L0IDxbyAufJT=IPT#~VWoMAwX!k&q{*y84x7YhU?%Nl2|-OMZ}j!{Gv+<^B?Uua zL?x8|N~F|r-6U_!H>s6!w$xNs1*U+*5AT}F93bzl?QRr}_F2Y-=#YeYh z%}vG-lqy>)#TFbrY0U-(a8NvM0muVsSaa(#afH|0if0cw<0}FmU1|<7{HzH(TP!Vu zRcft@uN^{|@xO_c7GnX<2jlA9piyH_D6w^olF-xZ*Ld02n$qTg%k>p!?VdamIM8`A z7ec`h^=O6Oc5+kv4?=pST28wG=zMkJ6wDb|AeZx&v8)BTia9l*I|nGpv3I}s#)ldX zz-lG5mNlTd6dQKe5UJ9k%7sN7_m?!~$_}gB|0|sY5LsVGMJPT>pE!bl{{9ywxI{v( zwzy+m57ZRcfoz>Crc3Y12Qhk8Au88eAN7fg^X8l*^n9+Lp;1M=GWBq$B(G>v8SUR^ z20tdImJWzL&bLlv^pgyXF!c(S$(LT~+A0+v{!m&v1UWXowrdv;ae|3d3f`Th74csz z$2%TB(2El6`2tT>gQu@tHZa~?X48j;XOlbWPYu_ezx4N$BHrzw`$TkZZp=oD$)mrS-*R0HE69S7!U5C;h2~0!%T&(*1 zpmu{yJWIcsln)4hBKYve5-`dMX8YHv*t_B)|1{7(674}BJSAuj%8*HVkiq!Ig4Hab zyD0c}RD&e98n^K0heMzyNY-V7sYvyJZ0ha^5`4wDDZ;QkVtU!+vJv2!e-YM8RO)Du z9UrNi425VZ&i;&DD?AUV7Xm-R4qrJM6p{tbLAM-n%5W=gDq>^HBN3~fu371hrd0i! z3i<+O3xx9wYZUHBWAmU(N{{2>7%;ak_&7*_mti(rYjol}w@-iKLwYA3Vl+RP%t>$u zIx_JHp07I0_6MMU2+%?C8NLGRO95Oa{Y5;z!$TFSaF+s>_tfl$aT&pku7sllVOc7Lp`&DU>u(o;4WpQ zIC>vAxke?P0&ai_JK6D<6NLGN&d0-y{6R$G&&w~ZfXiYaX7*hB{7AZjXL=JWSX`%m z01F084Nye9w`MVjiABB*Ive5zf8y$d-+fTO&!y~II=mrBy!t?w19uYe>M*YeF1EMp z3va1Z0!5LK#-`ZA7NI8u5Vm^*azDI~-lScUd6)4#s#nlHAhXUr*mM5?X|E!eSNgT`{i}@K_|d=1|E6PYr!e^L$MjC4PV6)!NWpo!nai122c?^*W0 zy$A$J$Dh*O9@1C#Fghu&hI4Hy7eJQCQ7Vgt*s0_Q6aTwx)AV-?QEy)Mv=z!3ig`!pY8sJw*V^|7+yz!R~!{n?(cPJ zcj`j$;9nQ3p=T@128V=E<|#g^9H$9|5Z=oHsB%US^oi$d>dBb6_b+gY<4+ZURwh5C zWQk*)^y$%I4VfVr2_JSz>=2be{)DbGhw9`X{=3(MYv&yMUU}7IC23ppQdb}qF?9$2 zBKd1;5k4=p-ou!RgaQ~RG*?4!=J0D2$y6|djZL57)e~)DlpY-0{h1A;Xx9K-5g)t$ z4L^Y*GiNI%-8UStJA|4?`iWe7Y+7Vw8}~^gk`>GK9F&|6$wh#e_7dU*FP|DS70fUW z(xJN*$Eazd9;9S&*?$6Eu-8RR<9-lC3X6`zpvaRhDnA(Or%P&d11*zp7HN)Pr9Xnl zJ~E8yj7!>WzjN{=8qv=vQGqBlprOh}$!Fw^vZuG=;Kd3fu?GKI0mL{>u;-wMu>WKW2C1);7@?8c)y68QsMYLIvDubwMC&$pZqXoBQB zAaVS|4KGS5UhM_n34%IDE?m{1qQSPue@At1Q5a)B`D0rARl3B6K4q-MAx|K?*FY`qvCKR&B@w= zpK*_O|1VZf2QcMBl2a!TLmGk-n4A|C-T0`&va$2azX|-op<13r6o$G!;KhHEV$E3r zIw<4U0q{<&8Jl&|9BqnOE*CqR3pJMHSm%^yw#?yPJ5-zubA)qluAuP2E7}j9t4&u( z33Wtzv*9{#_6dQVnlb!h0M%2VbFR<#x6M}?EWVme>UUR_F4bneL1u}1??C+P38>~d zw*MUka9V0^@w>ks{ejsL3egWnKLjECAMCz8p)zmTf)w1(Ijfe0c%b*XR z2CATCka@$dK1~){G(r1n>SWb|7T%6Q5G&?7U5qD(4xO`W^7=%0&(&uj>f zc94Ub0fSRkeuqT(UAf&9vwApRSNJ-h0BKplzW5)- zMgAZwxDR?E8Bk*F6cPRy9Gvvbyp8vOXbN$WEA5*}q4BX!ac80w#l7(*ha1WLntB*~ z*sS|^7okKZf{E~uL>nRIq7WI#lDW-gMN4)!>GmTe09GCuTrovCGqkL$hsZ#h5S@?E z2T}1qENLFBgJ8M&=V^_aAZ;f@Y+u_2v_OY-WTni&RKJ!hYkwrgOvZ<-;Vj%g1Su)5 zDYyK%hA=F;)v3eb13hp7wwm$wGmdOTnWpusAF1;@BPECH@!)B!9t0a0fHJc%g1=fEKg{60QT8r~!ixe+A+w zz#a^~+J1m((4og5U>gn92@%8*Hn(#P%PQ#}vPN87jRRGz2@M(i%{iYsLki;{1jEplm0AhwG;Ym92aD$4ZQ1gO zt#19sIg@T-=iXl`_YV5Jk*==$LBZZWA5WN>6{$>4wIfknL1IlY*$7&-1A=UwH(cd05>Ro(Q$i7b6hC! zbO_eI_OqM=6Ku_djNEy>7ib9*A9W5@-+^ah5!%)64U0}Cf{DRq(;>PT1tOi9{m3$? zI(edTu*KfpO^C+l1nI&zUk9<2?Xi{nea`h+tX2hOJ z1DJIBY0h3FJe6sEwDKUP38p2A${0ObOI`ghlvb&ctV(#8*H4$gy$HO<&4UC~| z_r7)lK#&BXtU15?O$>fakev3&Vn*mJL1bhSHpq%qn0TWJuh=s(?ZzpG5JFxq1?-ve zoLEjzm&*r@PDqh-$QP}NS*J)xvk{Mt)|!%U=`-4Api#}6;G~C52;Lz77ryGb&`u;_ zu?XDhu;~v~p-zzR>E;B$)6>_~`9SZS2&wpmfJ9iwDV+ZS=1zD( zUA+@VVsVBYO#M0KoO3fegul^XJ*U3&8<1*;J)Nk)4w#k?(Of_h&x4RSr#EiD19U`% z#fqC!fb`@+0X3~J72Lqqw)i)JL=gNNG!Z{EY1y_kr5ee7=YB} z-|H=;e%8zwkR3GD39`OQ^O&)`tl?_uQpCxRGgUB=qWRW~V-HASXLGVU%nId}*5bad z}M+C1B+@2>SykO!A&OaVa4F8zclJ`53biGG{nI9_J%X~yT_~L?md7T3 zmpUlQ!dQA|^fHKAE@t=bo))4K(x*vd7!+G76D@Bt79i<(t;t9HtGzThY`O=qHyQ28 zEVqw-T;FehH)3-aP4(G~M z7#o3mR2L=QmLbsZp~<;t)0WHy2%R<(@*+jvt1Od`Ob>f7Sl34{oUY;Pa!NZ#Bsd_} z-TZ*ui;-EP2coq>EDs043Cz$2RX4a0xJdg`e`92+9~IBMk%u$$9%2S9qS{9EiC9-+ zIdi(fPnGyZwGh3Z6H^K#d{Oxu2;_-yPz@K6I{N_*RQ;#^e&Ej{1(Dl0Ml;M`m?5Hs zr^3mli%Ae17TjACm6bWFqoeQv#~x+aDCtxT&N#_qlGn7lcQz{)F~+S}F9J1}+p%#B z6W9N!8KBeD4^ZU|!V0s&D4@2sLwPi|BZU6EuMw{9o1K4LE76 zkJZsOxnBIv)r50^LK$0a4wC-~f8ttkM;y`MBmk?U!zL*``5DWtv;lGy8BjYJxm4mA zkww;BJRr5zVN-D#sJXWlqjVG_&5ntqflz<}#}~E+TlZ`Gv+8hr1jF$wcypCEzj=f& zHJO$u*wvL8C_Ok`mcJbmtygX)bksx)v)z+Sv$RB=ntKEr4I`?otRpH}{MAt98CH7^ zOL|ZhZs#5ep1v`+{eL5=@rTk{-$shW#BgEpOosz|4pE-yQv3Zrj+hVdg!TZh^m~*` z{bX1Vu&e1671}Jr-O`DsneVNn%1qXfMpL!*RU7Yvu^TdH%Z3a^S;guf~DUfL2Oq!27ZdeWS|>u7O^ zC>F0O!Yw`bk%!`)d_*<|8JA3a{ZF0~i#^2^UIDYHV?}nMmCNWR!d44mg2rLX#QFA?z$;TmWana~vh6 zvBBR|GUP+~6r+gprwtX!*{!r0BwvJ8HxWaQ^pE{q?+K!X8zDBhq$+P_c8dIsjg-cI zc~z{151Gc8^2ET;>vY%OPcOre>qI6gmNhQ0yKgnMT0u@f+nQ4 z*&0!ml9nE3%rW=1yDL&)@Qz9RiUH!-{b}V^THd$nvuBwb5&R$bqTXOWcDih~`bvbG z)G8l5XCSm(t6$?*3($_Zr$6vu?3&c7xk*4ld*85vMSx2W$)j{ebHD`SW-W#T^$sBCv3AwnMtoKNavy4eapU{9E9gu|} z1&QNj3|g7!616(6=TjSciuz#FOKhB#={-fZ5l<%>*LdJ&!^@R`zD}sjaCI9QG2Rcf zZ_)pY5lPO=hc&7g{r8kjUJ{eQ?6Kd&I0neHsWR&qUhVR_tM!~D06jp$zxxTibz!fE zZwRhtt_euQvG%TF5Kqgl`SwyYym#HG84w;zOa=ImjF6zA78hUlK3XPjjzO^cC2AZQ3a*z zUS9N*&stcULixd3p#_kn87AVoO+>uZF$nPYkA+_^Kw{kpaf66JYwlm0`4|_2ydkfR zv6(8Y1p8I5kq}hfoefU#)hG23QglQ8vNmqq>UuA|J=HIud@n`${l6OD)PWb(6Mp)in zZXLjaM+>H%0MwQ;>UBSXQ6R9-q@)q^UNi^fn7*|tJyOb(Z2(J-A%b+S?10b#yL7Mq z8UO;G@A)p|WV`fw&)g0vn1CgqTRK9*r|QGAlU6GpOVZNY#D>JbpJ}sQjKck791;5P zkgf7kZ`qK2ZL)EhidF1fc(}@NsqCyAV>Il?8+9oxJUnN8g>K|;$r*seaTvl-oE?iI zmc8cgiR-++XIKh)fm9lajuNuSOhP6kEz+0;l&E`{WFSxf;r|uUKmXMmaK=bPS2)J-3uk-l*zdV{TWDzI7KL#B$Pf z#}@l_mRgW90K?4poqUI{lBmN9h-4Qj?iut}#!3o&2o9DI<(l`*cqK1s0m6S1-IxoB zdc~H*q`@xK-P|Z*i;;2Q5C1F&xEYsHsul@?0;ctwhaoYQF7YKB0-E?h+d&PAwP9so zEU1P4W`g@C++{Nt3X!Q%jRh7)rLy$mH7PgeN)b4qjUXx%(L0G8RQXHfO`*T3j=*mh zZ-R$FkVg*v351=%U~K|ZwmVAe%Y388dpvpDPAVU z_hiHn0#q2ufybuIkJQp~hk;FHA52i2ZOGlg0@NIY?{KHi7Afw}ONZ z;21=~&!4krH>tyzGz?Dqs#;k!FKhm>RPfs&goi%Y4mq%A-S0tJ0@d&Yuj$gt^bRq@ zn(hlg{9H<}s((!u>DXnQPIq;dprXoIW;5jzV<!sK^9qO?I-wfJAoRfz$xs6yD8Hje3+{~y)m|Je5iK#Tqx2+)hIgb(cbB0)%KU*P;s-JsW( z9s`+$54)kwi8h9YbHM(Hm?SST5!hcy^dS-bXM1-i*#YLf78%e9Nk-|rN99FQ(94rv zA4@*#B}I3u8b(P~n*b~$7wz-M)9tXMrLeL{#}g6UnQkPp5L_CsOc|s3F{=%H>K|um zU=#0oeka<$G{~jLB>kJqj2)e;aU@?^`556{>MYMAh>RJzJo%u8!lyTx>Id?IOD-6&2lu)BeVYtu-Pw)pHFH%&3R5Q6s?b))jXGO_Xjju??rH~3zMGoF({Ogrub_q8YFk?Zd~H6`Q`>$+xR#bNm%uk|EfWq zG|}|5dJjk9o{V9^^#kakldc(}0?y&eiV`Nf`9K@V6%Gfl;zc_tLZ&00pBrIt3AQ7h9x>sQ^ zT|C=**O;{`|K@6Rb3nvTy+8K4c=p%M>)@^P2p7Coj12T$bDWuVKL}j@{N0uqh-RqN zSIDza5dzXH=0jgbVt|K<%=}Yv*G(W+g<&;P4#3F%Ux-P|u?LEzFSJpl{2qZuDnKML zjR>ElVa+hf`bSAT`CJh-#t8qDvaEWxsueHpUg!(_FHq*XA--VrM*=Z}MT zIjIt#-IQQWJ-Bz9pdhN5p<=pKu<2{O<*&*hl?+llU50~ML`&)!%u=}B?Y0uk>aamr zO=B5xNpStZE4=n`c53zmdJfYY-6e4&tpZv75R^FFduG8ky;F?)#t*2%(W)Ki@nWG{ zs3X?TUw@Jy?_B2wJa~fzY|XLo(+HDipY)cqNoDX&*P;!JG)lq2?UtsnSKcSG`fz6& zU7-|&(bscdvaq(}da2LrPz+?r^;qkI?ck=O&1FOk4IiEpFz`D3%lU!~w6EaK1NU^j z*)}h6oevV@w=lM{o(f=`+(AXg?l9vgO1GxKtadx-!JQI_H!qzr*@t#i7r+S<2T08H zbgBL<@AXzgHV%~4BzPJryr24lCHGYaajR9)WLn;cKVw6dV&y)HMdFE24gke~IdmBr z6J|m2AOjAscKXRMI;-37s!sX;=Ejm4Rb~NNV~{y3>RTWj5G?8XS+Jiu4|&}I2?_5~ zIt>d^;mnGb_f;6U0GogRjm2RKG=>D_!-iw zE$Lrh&HxvGotdFdf%YveIBVJ*H$Oug_&H_!%e6Y2FNhY@6EV!7w>Hv@0SsD%#h+bY z*%lDgi})AHux4atpJS7|gNI{4p%rZ7U#j!h65nE_{w!PO1v8d9URt?2#=9hFzTkoEEgS1mP zjLjC|@%r)pBexjrs(p0E@!c8Yhyv4=v>FT)Y};<)2N}DpouL=~3n!vP$Z^Q{?I*_7Q;ZV$%NFS&ZT zZyn&joi+lyTN^fY9v4PXz>8U1it@M#))||}?Z>K0M1n1}UH@nn8+P71E04MY0H$hr z?+33k^x(D(iRV~%h#wG+=uZ7|!vS~${jg-h1zBQ3i$zLvek=BO&gGp(!e70ree5WO z9J6)iRkGslkms*nb5(V(2@>@a_v22REiKdj;|^(YkdOlvUb4f|%AsP!$oZ%H zw9RIW)c~oo%s8H*ROY72yV`TW6YyT>CGsLLtxeNaT{FW7KZjy%pWfHM^z z78frRpj_z#rws32EVvM~@YybbQ2Ryce*xI0?r`q5u|CxV>PLA+DQ!-=qqKbFA_`%t z=SwdAkz~u1s})Zb@)=`gCHLbE!pfOZKGjtbV`VaS>?OER;>C=QoByCmjRYCj^qFus zUbskWHWQl4aMxZYK{rxKWR{Y@9%Y6kyI`A8E_Ug`)P zYnP)-+ZnRDWmce}Ej-!{qQCi4iSbpWl?#jbqEDF8Z)Fb7DGQNFl$-Z(%KJ%;T(9Y^ zHA&g4GB~pegC9*4(G`vhv6LBlGR?k@ASiIo1%!p~oB-sj_WnzTOh&AKzXolbjxZM{f)4*f*m$euRGnwAI{Tf z4V2*j27;sC&_DE1_B_jpHOpI+W{7aMiF3>S zz^Z7=!yRGy;T1^=pnyqZfl{hhT^(`+xl4~oV2)h!>h?GzdJHQan@q$h)JyXJ?aC(0 zx_ciI>i}y$4Hw?Hk$x;v{9^or+MqI^HFydlxSErr5H?P=G1JR({y@pSZL%*MkX5YH zAqo=0C$eoc#GIZ}JGrSu*i0nDizD)2spoRn>&M{XS@^|M&~OT8%Ive`ql-8D0uK?n z!r_r2ayPcx{2BELJEJ(iC#EIP>ByZ&ztDU*Jt>0@vD=-lk^z&V>bI8WMHHzT2HSjd z2wfwyAbBiuK;eQ0A>Lp#R!tH9Zl|-H73<5<{ZTt58fqO_Lj+La+6rgIs1!(pT(s0h zD0m=j!(2EiT^p%_dqeOg|Bb==NeTyTn@;48}G>TvT>SD{O~?f?%)y_w>@s00#XgBd4;512v^ ziF><9`Vo_el9Hk1eB0O4^r*yanPvOQs6qEDyANrl`yJBI=w40TBt`}TPD&Ezs|VDt z2j=s)A(XNm$ey*0Aek54Xray2&*u9_l@2XzN9U-BQN%F;_r*9Qj3#D=Z<9TAd>>+? z663&1IFj~k-0^b;4EwQ0AX=YHAQO^%;bv=(YfhNCR+A~J@Zprc=7rUxfbVRGeeGXX z=T9Zj`Iv-u=_v!BdG5dZD;Yd(TBhLbEYdv{UXSwWvo zMI^1Rsv_91hX6Gan;;C$y~UPmZ;KyBDyp-N_8k5%E&mPgj5kryG4oK4C;AnsqP;ddsnY9ESBduydiSz%H)!<^465eWE$(f1M7evCq0BM#&~ zA%gQ$e7tqxj`TC>$8x#gU5J7VU^pdzW@NNt{#Ma8Is|f=r_fdYBpv0R-ZBE`XKW2t z1`>^hEc2*wZ8J|Q5isBasT<1@9)%OC(e4hA9ubirsn}Buw9Zkh0RZ5c0E>Uwv4_V& zHlbVH>c3DQObI#_?4+~)SJu!?l?UUYs{keLKuS(5+{CG&+uDH6K>&}phha<2v}h2Z zgyC1QJu}9%NqCHGF?RD#Adx@)eh-C_rOzQ-EnOwIbK4w=1URJtQWelF&6)D4LDFmm z_JDza)Odv%vM-k*$ed66BW}_@-USm#YN-ySnCW|{eEZQS*sJ7<(OG<_ z#ANBiWffO&=1`=wKK0X}|PlM`QE+M4h9JQw$WJatL*vI8BxJYxYmG|f7n2lARg2MNxwiy?DJ1(E_=;Kga>U3qDE)XDty)GMo(yf}} zICa*qzRO6w;|Q4Rl#ev47-Gk(-+RdF=w7^8^X2(im4B3qItsEZEYH;cJA&cb2?phN zCZRWcZL31J*(iJ{e?kxICRe9N$Focz8|0`YbmGYY1hXk+aIYhSG zin}CI&uoBk_43_#4A)VJMmvu$qQua`y$WS!=6NSybqBxpkya70+rTs$6Tsg0`VztK_d&jWBX<_cZB!^N%aD5zB{?8;^d8?^5uIuHmBa~ah;|6$(iz_C zX|q_7y0(c;bBRcFe$7&v;zS$8Tndix9z)KD4tfC7EeT@rE|w4%#77>HbOw%p1I;+W zpMfBMb3aBS#8n&zKgHAH)B+e#=ZeVN#)HPx(6OmDYwnNl?G2-m#so9-5A*z>oD?gz zv%2HqfK_hGD!RD&T_&##jh3NCquYXII=NK}L)?Qz<9AfcJrZn&$UGN^XqBdRmyCa+D0+D zJdmo~aycg;yqzzoKoHG9E2r+a=Mw|-=U0bJN|dRMhabi==N8IBn!7F=OKl3u3d5;V zs`pFzx?t?>ZubfIB_=>_-2|Iq(v`++HfOAVlKR!n7pt5MCLlgI-=?xCx(H{ajz-04 zLKs3`Qa@MjUMaQfX9$-)Rq$XBj&tUjVy#f(om#HB4N6;N#ahbBtzJh|m6a*wWad`g zWj;xmfAsNNgBfqO?p~mYu``Z5>)ouz$fn15(Gz#|c>TejmmyyL*R(U9at1k(KHqJe zE2pTL8wofW3r|X#4=QPvwJw77^Pqu`xk8X_(3a0c#pe|=#QJ{ejx;k`NJjeslFty=cx@8Qtt+5d&<}%!@)$ zOC5B2A1+K)0{1?D&_(^QJvL59Ss$C|#eT;?@qm$V>`^z|uQQZDQ}6O}HV ziPEFxgOb``flLq|s01r2*-+2k4zE$mZstkP-HD8pp+_o^qE7>ZWq#t{73oz+me+C` zS(!Z1CE$LzNe*b!ldk3&u~+5pYF7XEpoS~1sY8SJ=qPh(T0zpXtuNX+YNqmvz|>O4 zr|{aU@CADcUJ?w}1kP>Y3;X7>mI{-AQHi8QgY8LCCP7NLZ`Q+T-k8URo(`eza0fi1 zv<BVL4{<+9+Avw5Tqq!+%O|CWj$ZmYcYUg*Q;Rn?Z1hojN0lnBu< zSEN9@^-Tq;1+V+HLWZ`~+5=1jgRXt|V)pB6f4!))|010+%aEG;>LDjN1sTE#%FKA5 zUgJ4+@e*gu9sFCX?o$)L$eAiIoz=es1d*43xPytqI!!z7zP8AyJp|!QxL}}hACZc> zAG}SwS4UYc$SqfeJ+s5tmQXf_`WM#2c*`@t8mVl-OjOq2G7YP8jNH9%YU!A5d-xT* z^j{=Xh1NZc@A5S2x-P^<)U7Nl{pCnHa6}-%tec}(s1Ot8epI7T0-(8Q#pbZ|!@$3a z?&h+fXJ)_DINp=B2lIe;ztE!pgRr-~VpR5#m;+ZG8y6JzjF<|t4C||&Xj9D?c2sTg zqJ5l>mevhlbv+6fks_dX#b=_aBcA~o&kFdkvi%1GBwn%}B5^Bzxb9ylHNs^Gv6?c}!;N!B%EN9ZxU} zJGiPwz3=2isQN9Qw+GNBh{DB#UiTHDi3sfIh7OwUSVOR!f0B56>Csa>2IxXamTMu= z85sALAlTyZTor%s7y_=JuQSQ-&-{l9$(u;N(|~ zzFbXP@jX35*UwT1>Aj;t>@pKNBt4#k4$6?=LHbUzCJ^dyScqeAJ-^N`-N5EiK$IA| z#z%|Xr#E~)R{~0HE&=`Or|1X0dn>_)>ZZ9mxEYb$sQH{j8+qKUIXo|*b5<2*W0NZm zxMo|=eoJ$qK$ZlyT-IgMsodzIp7_}DGZx|uuwkf>x8KoGk7JJVRCBzyeD|>l2R|gB zt8HFgEbsl~hCPdY%;&HwhX3C1uoX@&bEbPCRvT=M78zZYG17bn*Rw30%FAu}J#DE7 zwhDo4<7*j?Rq+E~suGC6m>EyXS;(&~1uWS$)#GjzW7M`6Q@}WR8_w1*G2_N8X=uB! zx?*2?OQ-l+K<1Y_V-k7N7&}m#Wy5g{XVR~mSrEIYGX_`Wm$vtK z%2Jqx;(u`%R2I0gn>`rU=XJXd3aeaP00gR1*0-4#62o(jc=PyYFxDMZvb!m~_| z5gYJD1Mv$~rPE0+%l7=$`G!6H#-e7*OS78n(-FzVg&nf>;~FWNTM9AjBVIwt)W(B{ z@uuhcVCAErYiyjm%lz8k(Rtul@B#RkhYW;X46T%ke9D^K8SX;A^Tkqg5oO^|%Ov!_ z#1J#*#DQB`{T96DWt$yH6Cy+aU;|698h29t2TTe^%vAImFOQ6b@?|m{`>P1~Jk*T6 zjJ5;xNjm`Tf6SOH&)HZn1rpaC!ZVwA-pfzucSEpwI8o;)$VBnrW?B9{J`&YMBkoo$ zri8aztiNcZrB-XXUr@fD?EfV+*u z;Ot^xs1J7W!hF*FdoY>cr0Z{1L3w6`EliHsfDOkIbacrQOryQDP2o5I-6)JKo%sj) zftb7;e!2xmIVuWlf}oUYr_xoFPDWWw+7TtwkSvh)3>5b_ zS|8)LpA9s0hb>FF%94muIR2A^DifMAg{VvcafRm$n60m;y`7zXbp%Vy5}Gn7P0d4r zCB4|7yPHaj;W%LQ|2y==#|Ld=UnHRALM7sq$y3-OsYcc-rFCTuy5RZ1DJlNtMa6ZC zHc$sHI$cs(FI{x;WdW-zrjr2jcDNW@O9evi82aRiIjb$4&0rHABPqZ-txc)4kN|K7 z8p&*K{4!b)qzNZL1O7J+3FjWHXGLl;*xevC*i||4j+wyFJ$kyK0+Nra|3KDJ!Q9o* ztGg=hF&G}H(9-;!FVYU){SeGMYE^W$y0FrrGH(!eXuP@zsivLl!v`^DNj_>@h6UoF zIRNcDKT9Y9Dmv)WkGEW4bFYRlbqIj6G6hU%^#}noO}cPTOcTVngcwv* zVJJ~kp@AfHXZyYd@MnWt(u$9(97BtbxU+F(dTO8wu*IP0r)>dJ{{LsTqZPa)_d-^= z{xk!7(908~hDchf0fx!Pw=qsE@*Bb13#l$by1L0;^2i!Sq9Iy#7XtBecy5UIIq(ig z=SG%4h4UAwWu+hdXb+ZYes0Q4RYjn%HSBaedTCGYHGvqOj1LypckAdckh33fVsqV_ zd{|IR6WZBnNc-&XH$%vmHU#g;Jls~m6ai}%(lPfh^BFZSm0}U_02MjClpn>?(W(G3 z)SKR~8Ehvk{MWa+lqzp4w;}HWd}$cJ*9~cPViJE<;n*wbM=J)2eLF{? z1*Q@dC=B%`q6V;HtVRo+tOzbujh)r2LjgbY=>a^j+3vjUxb8P;Rd4%>WVYzh>X6~+ z4s-!<-{iYgFLBkd~eD9P8QNSKILt zmT7l$cRGi_e``6+KlvL})(dJ&Pmj4Sa`40u<{3BMcQ6Fahk%(urjF|fn~g|w**bK% zS|kWDwP06OdCawh;|{ZN*O?ZzQdE_bOK_zh8O+#9lDFLGcn5b~r5F0M<_4VW)upIX zdaLT`-zxkW0uYB)gM8QCGdO-5ywgOh7VZ<{#@--<1HWIQ< zFe^u+=nqX&{M+5W%iqUr5dpG8B$OvDn8Ni}dvIOGm;w#3*cLL8Q!s-z1V-{8h9XO( zvjjfSVJM`qP6h5koOtnIi_F8A_RhP+Mom^yp`qn;R1%YCE4rA%nwgPh5tO{r2 z+B=?=QxzqB+(E-LRZHTZ_fn2V4m6YJxw)qGSJ%JXdMsV|b|NN>-jY|vuu zb`94JiH`H`D3Du zcOG{^*o!laT|tE}>kOUwetst5!aq0uho6j&!4s&)MX=}|YZNd=%1Ff6WX4IRWpe+@ zamX6`vSbR5GZc7UDYeaNs^nNqp8O7>@DenNPGvf64)irms0Sc(8Dey~VEFR6L>}KJ z-F?#pBMq5aF*|TCqWz!ssUG5c0QCVtV3?5WSHs_Bfun&jr%UmxnL;yl(EnB4Mem`H zQ~}38F_!6gQ%Xeylm%H|g9n+5kA~nOWv5O-KspR73g?_8U-Ua^g^)sqn0lk=(g%e! zX*|Wn8F$P1Z4aVakne{)RM~TuDpQ9@$UHe$JFbCxsCoE_XdDmp{K|=6g%T>d7gJLm zg;DQr(s_)@qH(XBVZhC}E&c#o#)%Mkm!YgUeS(>wENRj~2I*Ql<`dy_h$j1KVtbHV zf^y_=iT5i|MLxebh#-#*1Wqe#s1)#`;GCF{FuVAicgKj(b{RzizNUI=5%G3)I;G51 zLS3Ovmew^9fic^jV#wx~8iepy{-|R&TMlG?9uoFPFy&qj$(gTNra7>>_$_oKiX<<-UAz(Sv=bh~{$ z)EIeR>p{e`&q6~QYCphD1O2$Mpzp#ck-n%W-R;4(K@Sm(b;=*{a7S7#NsJJTdj@nx6qpaU1fzpTtW&d+QHC5d=uB|uGYa5|NR z$~_+EIc7O*__1wSrUoiZf;VlK1BWR-F^4rqa(M=1L&F_x&mZ zn7>WBlPqsO5uV(&WXuiivHNuj#2IWj6uIUQcv3l~lV;x@WA4Vk&>p<4VYEE_Uoq0QNg3v%1QJP-;aTU* zhZ_ME@0Tc&EyHPY`c{eJa1bo);5jy!)zqbO(oPXV=)TcY8mS)_E&_yR03x3IR znE9ZnSqhrMS!I;piTx9)(v&c7J;CO2d?FQBt4)BU_MKrM1fQ!=MR$#n-KxJ>Cyv1{ zYX)oobAuIM)&|_79N384=%p~GL*@~ycbHQKMXCMnQ|rx$c3ZWpYN{d;NDXn}uNmc; zS_b-2qUtKj4LtjUvfQ^D_rFwfwuxa61@Tc48Gx8fs+ml-PtKp|TTze1-vYlWd&23Z zPN%YR$--n80MxC-YD5%59X0_HOTi7T4*gKPP_hvZRJRLDWJiB0XB41{hr1vc8S1zH z&I%7V8$Z~MhK`UTZJ!i|PYLA(N1mBbnAbgoZBVX{$Gz!tCJsBA!=Z)(FsvUX2Sr3{ zh-M3aPY;O7-y^IS`LUuhV}KT}1i7QNR$8ktU0lY+K@O*o$dOwDKiz|R_hp4+ZseRI zl5Daf1#PbLRIyvf9t21%Qimg zztp)}VE+b+AYe*A&uzKeZ5L;y0Q}Z=@93Mu=M^RS2F3v)-panJYUYJW z=J|`!<7i79XCszV~~{P#ZhMP1&-whelg^azr{Zi%;r@}Z6ZiDIiURJdYxvymH-#C zv>0U(H{&1MirK@cua^ev;uzjH`3vV&0*My9x%+Mos$KfuNNEcROg5nffL&m4nQ)!-c($ZCIBWK0`(aqlsr3=uis?XB;xF&ydO zQnfI~d=*8WH)x8GDug4Ozm0axm+QKQ!0^Y#lNtf^Pwy&l)e@hS_z41|{9+u;D>;1u zcNv8=w+J!VBV^fKj*h>6FV!v9aUTwgpg!1$Tl+u9K>*0CFa@$YNN7_lg+`SswS$e= z!dteBFOcc#8ttuGm_O5Q`|k25$BK?-UMh~X2^O1G;pQE=a_&m2=-k)=sXF-XJkQa` zn>Az~j(eAu5+ASDOX+DM5OmO|S>vB}Ib`O%dUI!2)Mh`#25!HnWEXzXbey~#m{is#^*o|{`+z6TG z;5_uxY!N(VnMp`a;t^}9hcLp(kW(gM%q~ISxgZ1fAhJItH4+!8IQ0M_xV+@@>QW%1 z+>0Fk08Ps|X$bSuud!A~DvAN|e{WA=xLrgM9~8FxYl=vRL-{s#vJu^wZEi%Uz6)LN zLdsr#gBA_Am7~49i}Gic-Pk3CEbC@u5A*>1wcrStw(h1ep;J~zF#5NRxm#a%wGbB1 zP!r5UA&hhdH^!5BAi_;bgTt(aiIYbf4R~Z#405Eny6G+b|Ihwh<&0vuXHqpfrP&uLK`R^11z zO#pq;DEEf+1xv!WO#tPa1y|XluBBJIQ3ey022E$;MZa;N-T}DldMG(u1JgkCZ8s81 z>CGc8F*VU#bfa_lmz0n_h7{Ps0? z(TpJ7l!Y1vzp4%}?Fm1Q<_4WjT3ocb%)K_)92dTo%nMJkB(w)4W^*eg%zXtn1}`SN zh@;E@mkxp+EU#C)h94PQ5D zBhlQj8M}+b@@M5w)(ZyQ${5l`s=+`gQ9A4jp@rhS2SR&mXEcoCpt-}a%^uspbZWrW z=~Q~BKhkLNa+7B!s}`iiX#~T5LdJb6WBO&{!$Ka6iV#IvQZFGJF$Ph{n|(U~#~cPl zUwW)W$$WG;bd_BlFQ{m4;Ai8a`GiHL96>>k^N-uR zRevzkf{yC~m+qi| zG!S}e2HX!-RK-fmn+RS1(}G29=~Gq1_`TJxs6=%eHr)?b%CK>{E998o1YlTh_yBh7 zwq7KsSGgf28>`Wh=vAf-arsTuxg)e^Ps5srQVP23*xnuW~L4rGqcBiUfdI<({DX!o~$cf@d<)Hf$HM>Chn^# z=B%qRUtc*ouhzs$) z$~h&nbogUC4~UZPBZ1<%WHMUY_yP8@h0 zVKSoB2ZFl%R_~xDacxnB!cE3Hwhzf%5C*TT!KVDWKFOapEZAJW@g>&=$2?N?CzB{E zHo&=sZO`e~!YtJ{zlu0$9(CWjEALHsUuJ2`~@3eAJV zFQ}3fJONwj#|A`T2MK47KmV9wwFH#8fTyYq8{b%u`YtHNE~`){l1R{-WDVVo+D$R5 zV*8cT?0;+Ioe$#()%r7EKkF(-@DJ_~emhkua|V~no&zunJrS4#pVBbkPQtRtK>I!Q z7&5smAO$?5h0>Q8O0x}pwn>MGl}Ccv1w$+`#l%La>aIL&Gc6loTep9!EUiHY z@)z*cf~EqRx@jw-aIYsZywvlG2rsYFyJ{{i2{?dgk3YROVT_I-sT5YG*pb^k0|}z( zg9-^H=e@Bb%EFybO<*p#Ik9g@lZf|j8*yWswF8=TKab@mGHAPd9O}-v{}EavH&iik z8T`zVda(PBS!=mq%-PW$EOfLW+b?d8%HQn((2)FgJ)i#tl{~&)iUtd+?4(Rf<8P?d z$h?(WmtR?r>Ujo04fzOu3o@S*K1V!8;!QQ6RxE4yFVVJ7assoPj7lx%TiMWhEb>r% z_1q$S??t~!3|hjR9@GTv5$ z^w1(GempyJN6ASEmZ6$Sq@}syF&6O@Qa3*tQp zUgn;)@+(C@2oOJD=$?patFT&efJg}GVtjFf)C!Ni1Dsk-^s%dGXbP*=Fta@WKj$?? zwnep<{}N518SO$vM3pp;-qse>+=7>PB^Pvbfaj@DRK+d;9Yq|8u5)K|=|1&O#Ebwr zCl4JkP~uH)v8=pf=1=2o{*9p3vP(D1G^Bu&F=s&(LgKE#>o_)`1Cs)>ArW2+wGg8b zGET7TH#JT{(X-YdzjWE~yer_cTfhg7AVw^tdDa)=*X0#_X9(pjv0M(? z6aI-JfST)D7+z|4!o`Ggy(2mWeIFWPgBe)3D~-c$GUWQIh<}w$?F3OBrQ7kAM)o_@ zwhQw%`@6Dvi>nKp$4rCWC9s9qF!iuuqc((%OUT2@NynSAaR*(LfvvYlNCwO5;*1=) zS0B3nzql88B-Uy`lKR*x$?sZUM1+_igAEfPHs&%dqVsrY-u1u@?X{ zl?=b1Hn7?FX!&3U8huTfOl8b1M@^L1$r)*akHc1fId9Q?%YKfoFrvYIFE~IQ7~XHz z#NJX!Gu`e@Q4&k!LgmG3?pHK-)&tdwm9z8;jdv|pORKRF7cQMetDYi;5BDxkKNwU} zGqL(8OccU~K(sI1KH9#(n-Pd>RJ@yupKLiNa{CK|x*S?DU#>=d%0zyP*}T1%dt?Mu zsr?Sqv4$X)mzTWQR0*IVt7e%h?V(S8PzSYTB$U+fImC;MF_h4+3=C$sO~id0Wj5@} ziD?OXIV8*|HVVXv3n)oOC2lMw*U^g&FjSxd4QzIdgLPbpe7ff+q3?s1g$e8naKN|G zRF%jLzRmejnbg#T%H+(=T+3EF62S8$>1bI^)ZAw~LebT-OvQDEj|;?3?|2}XwC0+E z$)9b`_tqWyIe;md*wmbD1#b})_EW!y&^*`-G$iaXd4F|W7I%Lh~$hWx@ z$!WEhbwXz+yzCG@;{C#fV4HE3>p;KI#!s*Z)fAu=lXiBT#-jmVS}IwaVd^}Q8_0v! zu+Jj3*|h{zkT2ojJ{e8WZV*dL%%b$YW(>!TZMiO@(GI3O8~F(9>QP##tMH<#IA%v; zZCL%BHt^>4&yzQ3`E`J?$ujktunf^kj4TRwvrm9P+KnPkuI8-tHTp5TK$aApKRfHB zEJACmT-!n{yzczPmk6>3;1KHt&+Xp+awq*2%NK$u zWVfr5Eusq6K>d`xNkmr+;5o4jEArrBx&vm?(_SIYyg4cGnG^>2@6KWqQx{5PS3J16 zH)}&erWbEmPyls*a)vjl;t=&-yEKsX^her9C3LcHiinBWkY3A5Pz4tOkI{TfiM~y9 zzB*<3>p%cSK)S!%HDW6*Vi23zPCZfy&n5u!XR0*N1Wq_KN=>HW;R6zW6MRavfYf7; zI}f3q%GGwv^34pc?Ia^>72_6j2b>6Rx92OXV|3@8m8}x_Y4sEQZ#42~78dd1=B&72 zTp?tk((%PAo}l$e#dW3jF#}x*yrk^$0idR$Y5%SO?KTE6DlR}2Fx$bSw)bz{061yt zi(1nvp4&qt=ut&UQ1(6Q6bP_1*7y;)gCw8tN9Yb$`kXnNViWtIHKj5lZ-%($BP%(Y znc&MPZi*(q%ChS_()pIp!!`+=ypXtgkkt}2!H%5KTFdaI2ORXM)L%eBU{$0L#rNp` zY{qKY1=uv(cVK)vf*Hcui+fxfq1FJ2QI+LbUox$l@Do|#LIQibNg~90eyb;wVdt1VP&2QJ z%LV)bs}NV6k86}3@p%ULE2Pm!=(j+&!yE=>bO!S1E7=;ft%Z5NJBNRX=8RW34cbNr5 zC(jm?YY!nr1Vwiab=@?}_;evDQVw$%_IX*U4%ZvU`Q-k5Gt<(oDpuU8*TOs{D4GVsHyJ zL4okidj2Pc5W#@OEW=q~hN!rj&aQz~4*no3_%h!C&0o9GT@4LW9VFOCOlDtyDZC}u z$*s@E5!K|&d||pabZV$-5#XZUx`~bKT2gONjM+4j0a6*YBSSdh3_yL^S}7e0@QI4S zM8rF?#K3nC0@26qbs~!|*Wp|KpS7mp)P0g$Uvc z$rY$qs)iqykSwmBVuBhFa&(^aje5$Jp7m|?T~oR!F{6Yr<20(XIi*8$ca zEh)%>!@HR5qx7%68b$&f31j>*W&9*N8Y(?!_DdtuEh-WOq$vfQqX&vM$^;a2r5Z3) zre5($87BaE-?j-aGLgq!Tov!)KG+F+{s1~cSM39&oC)Ye-|G^8OgP|@27e*LKGhy< zpr7pEgYF0%FL*#Kx?k{u(##H*~bML z*FB1c(e{Ec+`|G5yBBl9MppAflnb=k39J7MCQjq_aQD9#n&z3Rp!{%v5C{_LPA#JZ z;EB64lqg8LjPOCCiHRez%{iU$-}Y{HW^91f>qwZ+CpAEqaa=xqOSTWQ*PGR7Zb3Hm zKG}ya20e*>JKHx?ucK!&`hGyL-XQi zY^~vjd}yDQl5o9=EjS03ECQ4DY8IZtr0G%TJ7rP3NEB0gRn z@bMP>VnbWwS0Q@U^FtI3*8+$xPi+=eu`B*Jo)mDvs~5f$i#I=Qu=*$HhFX+K+mU6M z2Ky5(Ax>0iXc7Xwl1XyLP7*f=w?CZIbVco1@`#N_Ha6ykT1-HwHLMBJe70@>d-;XD zR%hp_lXw{5vLVX$Y{8-bmp~lAIFxT3A}Lj9#;D~svycHtaQfBzBP-2l3*QtViHrh2 z^kElrO9y-aeVE7!Jr6)IMYQK!%GfXkYmIynhP?OzTuA_Cywc3wS$qsZB(6fZs;m~b z5KgE>z5zL(T1!7|=m%2%P2Tbb5|?%iJLkKVZJzUH`J4?nAH-3NYKgs z3NI!nAqwyyjV{|4R}~eX6wMAMt_sMSLe?D{0nK+5MbE0k3mFLEL`oEL8DMWGkl@Y~ zji#%1QpZdmQg~tp%!Ov@^WmVi`k1v?>y7IaliGs zCZrzyc8g=7gIS!Md>W5xA_7!;D8 z9o7$t3g|9a#M=*K6;F>#4T7;RcM$=OhfBLzN-b5Ei{YG^LeEij{;+ob?kYx~53Z&p zk(k{&)3D^T}7w#8J7><2j%_yxCsZ_atxj%t9IP~kkaKD-9&d^~ri7F3w znAC9BK9Ym2v~p3f_+JWKKdfz{o3tr_Y>?#z)A*3PKqTRf^YACkq`{4^FVQr!gR3$n zz6EiZw68rO50(YvJ+tHXa?SS~pO@Pi#6@DgHwvdr{Pnt1O%=VGb28AaYK*nMzyT;l z3Wu?Hq!kc-3Sa;L&e@0OthP%F`O>2(s4tu4X5E5pb@c0tOl1)3#fEhQ6SHVLD;=kS zI71aDDPN%ook@Pg<`uxV;0|@UvEMk7t3}`DJ9u!x5U9N^6uWeZs!>QeotM7A`4&VnrJ> z8u~Ri&1*k!8DU*70TX|3?;KO>1kdB!d!rb*`o}uNp9?vUnov+Fp>_{7Yf@#hPs8wN zK=fhPdRV+oDF_I$5&ttU^1$gWArZq0c!9@3u4)zW>L*&+0=8RUaclo^raZG){VTei%yT#K$tH~KKODf`8=z>qgzG#Nd`hx{GnI8NawU4au0_N|9=I0jTB1Mwr3^cxW222wL%$PZ#)?uMDB zpiLW==2(@3oap-We|HT=SE%A&-{rJ91?*P1>`A;!im34uN6BM zx4<7(opcx)Q2-YQOeHvDzh_xD3*0=16U*imFz96Vi20<@G<;P|6qxvCBVBLsi};U3 zFtD9dDSQwK+#WpHNZX_)7VSoDl3OAuMe{I1o}l?nh0QWVU9P9*dDdpzJ?9lboxk9W zk~j;;uGURWs=Q|gjS`>J$GeALnq++CG9}6VzA`O`SX>BizQJGbTf7NJo8c+<^)3@u zJ!&ysAh~vB62l%2*ydEsDsO`ii5YXKfQ1cgNJ+jcuOKtFXbTlhN9e62Zx{t8wDcxu zi{dIFIhbKxhoW`FAd@&zv7hv4Pc)?d`9+F9KLj&wNNHU)2D?eelI#}tH@?1#*cr|m z;h+@vBY#^=ua9lsEPdXJU=d1~0KOJ_k*h7g-F8-TdBl^4@4pEJ0&DYX31>uWZ;-&QT$AD{Hstl$TbkjZr5wl~iXbDQ43Yv6eZY$`d`i)Cv1411^d0lA ztKsn)HaV+Dq`$(QImpgQ}$*&75~_^q=n{ zbo!P2YQ}MUx8`F5+g6)O4%HZd^;{keBCLd;&}db@HxX?veah~!yZ^Qo?jnt9usH4a ztot5`C+>=F?DvvADW?ZJX7^0iyrMm|peHd4d5)jtTD7;0?mwvg!nO>w1H7Z8)f1p! zfP&r?=Zo9iK`aB~DUI*5IIvbN@suRJA~N5>5VaQDhOQcwFqgC8|vBd$P^GWK1L z-`=Ry6`dCoZ6N5h84LdRwK10@`Pejhaqy_pXYC5VUyxsNbNe4YsGrrh|IVQwbqlg| zycP$n>9wdFMYrR-zOt#z0CtY%7hH!~{-Ozo^?G55(~1HR0e{YGxXAgsEU@jEt(zAz z(?-%*amWzi?wff18fV@lJsEseSzbT<5fi2tw;KtKuivchmk)3`llKjg6EA8;S&MM;Rv0xajz(xCl|Xx z#BMQ0wl5}vM@md8k+8y}&{a=-*B-%+FH4%LiK9~0IZpx7HH_swT}^nHpgK)7J1?kJ zM3=vybU~hYr#)A0N|demh==Q28Lq~Pl)B*-z4RGsvm9u8DJ9bdp0qv&p{$ejux4DK zXU=?4EppSRD-IUE+BS^Wfw~~~Jf(l35WyE#!K-7xPrFYRD6t?>eEMY~NY^Y-K>l^C z*Nn*cGeL?fc#b>4j!9f7SL`J^qV2gw`yz_)NR|>#nZNZl?}V=qoZL%nfbk zVr9spM;V}@`uotpS1Kq!cvczTA94=0jR~@8Ds`G&Z;l8li04?jr#?L1zec6_fBqMI zd_Yn*u)}^xx9nfyhOPn`?go>WG7bVmG<<{&{u#yr<>)UCTly&FzNdR&9Kq`0&;mj! z1;X9S1X|Gw#`+KopMZUgdxmK97FoSlw!P3|s$vme2qFa&J0Q}jGLOvtkF#W*^9G$y zu%{HLqe%`M34e^}v!X-;xi}as*Py)A)z)kk@Rn3@=cf;#&%5PkiQ2g|$48Y3h6Qm) ze5e#+KdXGmU%{G2DvBSR4HFyf%tiacSbZ=Kyr`_S1HxkUXc%1N86;g&7h28R!1WfG z$!=W7cWcv=!LK!S&2ZvRg&=cE{B5}-P;mryC0?M8HnQwazTh)Xqagh$TzVOY{7orM zVl&9`tP7T1HHgw4RD$l5DS4uH7Oknqf%$Jb2_q!K7d(k zRU4hk!l2o2gSy1Q`&FZ)2{nO3gsKoTgvUDIk{7&7afG3gTr!%_EG=E1sSFbFyj)N>YyldU=Uzdttxe7<5KdT@!SZay9$ zSX?44RFg?s^JE1<`l_B*r!Bds!GLDdWtRa@iF4y=o3~1-?JRFvgjQdd zpjGJ-DT4rMza$t-x(#I$?mEiD4*(2vA}fgId~s!e<dq za21ctP6AkH@_M>V<*|MH5q)Sk*y#JQad}2#1ON@K-UHs!P5EZ6XM8e=aNP=+8$t@R zPAqeT?ErA0__oCBg?P(WP7o@s1p!IBJKS9}$d;K!gV_K@WMMi`#C#Y9r_K@46wAS( zw?b!Shd~`1TS)a!ir;Vwmi{C}z*PN(tCAd1+lV7z?FR~1CJPdG>Gm2SVI!SOlQ#dLqz`R;9sp*DX0j~?C7tz6Ldo6mBGvW_wJ&^>uPpns5rt23z^R& z!SN1s|1GItf$A3W$>_G~tK%cYkyDAB-P7W{z;LDOS{aiL5ip2Zq%Hi^kDTNWYTFOT z$K#hPvU+hZfS8I#Cyp{57L-RF8WsH7H}4yR&@4hm7qVlE?VR*6vEbm9zPl7QRy&`5 zglsi79XW@JHjWtn^(I_hQ}(6CsH#k2mk=q;Kb)e+1BPczk$n8F&uF2ALeC58l$ARl>yuz z2RZcl1l370%M{}Uz`#C5WiVQ%%y~1ECP4dVp#7>65wB8c7+vk~N zdrs8g;X?*4*uz6g%4CnKkeF1`x$dFGa$GXIpvjy$KK-SH@&x5N%W^(&W+R*QAc)EH zmkt7x(8^#w94w}LjusJ~`g)C&Qs<(Ksg8U+hx&V-m{YX=ux!Y1CDaavrXKEhnDYbr zm+C$I4pRlxz^^`p)qHYS2TGz0jS?SytTnBv0jDGLSWIOhv`NnMSs=~tuw4;`Ti-06 z;A_FdgUwoFJPf`=9dx5uxV6710P6=NKz#SK%{0;leMw%@-a5cuW8Mjgv?!h6Cap?Q zrZ0Kh{J!S5rlChcQ#hRFMm^h;{m2^K-|I8-B6QqD;NWt)6N?y819sodcS4&f+(-mn z7`WbAV@krSJd{|Kj6JBYRu8ut_7#XAvND%_^lT*{-j)VB?2nGE(^z0Y(g{#*_GmeN ztq)o%X5&xwYiG z+L08cdQ$n%%DG+7p53m~wJkYQO$Ba*&c7D@@}3fwD!*_jFX6yEYXnSw@_w$mM+_zQ z+7Mm(Fl{0{!42+s7_Ft0EjWGJ(^~_GsA1jpU2kU}|=s{blcH z3gRXN25RQ@E3y~STDck$S6i{^ds`I)*2dnX@K!*Vxe#lojy~!K8#lrDaZ_U^6hQG6 z{w15P%MlCr;x&hYE@xdqbo(>P7*BI@cxD?Q6~eg~C-(Mv8KYUoBsfKpcCD478p%L5 z%K;d--*mEKm(D(T9w9=v{hvXc{>`j-Ef|a8X0$0$#GgVO*>%YiI4mA9TWt6(D-#7b zeXMzY)*O(&&#>Z=Q-^ABDI<=gs2fgBImu9MfYX2{VYp%{*z6scrlbP4a=WXR!u>9jf>j!2 zZli$a;uA&_tEaahRQHn<7(no zulO7ui)Ec@NtAh0PfTlf_)JX%xc(De_NC&c)qwghw%9(!Xex@WOr&k%mAF$|AetTu zV4H*!^?_|Xs577<7iPvl`tSp0x<84KObPJ9+h}qV+u?ztMm@zySN9awwq!~|n!NNm zueJKul^gS4(beh=na&C3XS%zS%zc{)5c&AvcB}iem`QV_xaVJP1N=W8gvV}!i2)Z| zN4RZ>{Dl|_H4zVJH^R7N1EP+fs*HJrOasl+(zwu&*O_52tIhp+Os5j#EYJ2zsV;?q{{@O6C{bTQkQQ=30%kxPN3cLoZ@IM{B2U() zviJk6h6l^DJfl_Wz%et#94?`8Aj@V^#uofdZ<9GhU z5Vs#8&%mNPT_sC(kY@tA1^eta1%~*m|CtLt757pJSIG%iUx`M*3ti6tk;I1^$JdMm z>!UsASR6KdglJjetzWp{XdP_@{H7yS`a{bP=yn;PUT`7MbDW{{e<_?GyM%d(*AqgG zcgtt#(H^TTh^E(xYYa$)OEz8XSwO-2#JL!hJ1qP?s9vQ5tkT<36GQgQq+uAHV2WTw z)zjXX;&2{@RC2#BK337s;|WL+7zU zJw^FIbP>3;6BO04@zJ2P7~2sh!|jp^`D)jcI|#n?yr|Rf2!uWH@coQoBZ(!P@?WR& zwWaP`TrSKt&G2txqWlcVIA)!wZyMf_ZA0a9`R3UUHu)-+3bJo1LR}vmv{AQ~It|m} zA-Wviq8(`Xdb$7uY%GxffVUDmY?O{ZIb-lzf}M#03a|Q15P4D*(^RE)RkfI-PJr-l zBo)yi!Xg7Oo;W$Q(!LpZ{T*a%f-A;DvSd?}LD24Tq`nt@E39PKp&1RJl9CI8a9FEX zO9Bn{Y?Y%Khxv;+`zm{_v|0OB?t|$P=sMLg16zSa-@jwrXt3)A@weEe6UiEEDwNjInDccC*>JH8-SvMa%&cT_k5UM6(&=<$1-Gad5r* z|5{GE>jTk_XDx~8PiuZHznC=M0kE`-J1%vl0mq3?*_t=$$)p4WIg@kl(00opExakB zJQ!Mq&ZF;|c06Ft$}64{r=GQrOJQVK9tD+7u8pg?eBl>?4L{IAVkFseb1P$L2Q>DjBrfRUQyhWqbpO)k;Xnus%an$4CmxOjwW*8efHb!5$mp zx#3?2*6DFw^Si$AScP2aO|cSUyUqu$gu>B;yl!#eg*t`vcUpbOxRj(sm6|0?-&;&t z_~Mt3U>?dGe(1#CVTMK%b}kUhQ2+U2*!Vel#NC0$=-+ha)M~u%K07e_r1O>QTvwc- z*+PW()T~+^`trsr`H>Z!BFP}pr4Ef`{f+_i(dY{rR*e!V4aSQ)?SiPg<#_m#+hcac zUDM~XXqb;?(`OhTDtS_V9(R=U36f-c$y5I^l5wrlr5fUqZ;aTyR-(>zzSg*LR7pDi zWrTcJTu>R~za9qwIfIGdEGE3;<*rJ^M4CW?{vkN#MB zL1hC8U;NMxQ~jKxy)r3tEXG` ze!Mm`-RI11grW!WyLJXc*%HXz1hJT#38-rxT@J z3ennLyb26cm&kQytR(;$&9n(~GDM1jg@0$0RC$P{fNyw0Rv8a3(hjv%8XE!BlJxCy zMmgHg(WZgabalezpeGWcMmugnI(&*c5y=w((#7KGeBmG1L48QI=i25d#~kY1f_a9B zt$+D)dREGPDfbyKD9DnCEG-^{5jvut^j+|b_@wo zK-?CA>Hh{~OOEDjzmWGAJB)wKk1I)GY;7y@R}SkhgT%~&>vr^Bo;GaiakI# zFNz1pYB|C`^33}zp4q%QPb@34B}X8nZK7L{%7?z>o$UGtS@o6Gzuf>ljgO zqL#tPQ04wE@y3`(biOw8%W&B!q_Fo3)BqRrvtqkL&L|I_%^gJCumQzHh@>Yr33VNb z?p2H!VNL8qhqE6enIHn-jMfPb1M`RIA9OWkTxtZ_!XqoHL`zfe;(UE*1%**oZwsa> zzOCnCJSVt^-T|EO0s{Exn-%F^HRPziB)tk>(?&Gsm40t#Di7zp?cVH!fK|RG19Yc< z<>Vv}l`%`T&)gA3vo**j97+50(6SD9e={ud)Y9JTIVBm;o&kiWj1ufOdQl zsYVm#A5EU$UK^mSh@zTz!*KS6I%o@PVck47IW_N z!(cfTFZUk>8YVF#RH}G9MPS1sO%U=drq!owyksVlH(I2t$Pfd(Uk`Z*U$QpRkKe$M zqtygu-2`v=F31+WBIPb+O!$xrMB>>|#_dNd4tB7rhMWU_sJuI>A!BAo8wEhy$OtT1 z+dHxKR3iO|5aE-?*WMw^QDmnO{cNL6KdV;sU-1xSSaWAEM@U+}*tFk|`xL()X5joQ zD7VlFE|Sz}b%sBGC1K%9BVJ?q`Q64=kCGAF4Nam(wsoh`+wmgV1$dk>-qU5{?hcZ zb;JoU#-fs_dod6m`SW#6kNw71K&pTn3}4`Vbf!jwg9H-g$V(8ey0D0R_B#pYDuxsA zv;96t>GF)IFa^CrVVv$NP;YHsfo%_H4!XipANOZK>==6m-VPTT&eMjy&1Ma=k=`o< zNgNJHrlJyLj`w(yK;kHI8#Xk=nDUMX?!z8LskNzQUq5HL0&ns~xSkYCm|hd$Av?v= zdU!F9L^fjA`0LEc)A}zOM^hSDto4F06(<2dj33j~08;C;(WniFRA~E@_ex`E`*uzz z8qS(k2;M!J)omfQ*`P1e)a2+QhVVA#>Hr_-89_s@X(kgTJY@)gJ}x8+2kJ@HIRw2K zKCPJ%nH6WG7GM9Rtsq;9Hi^y@);>E4t!T0z{JZ);vp23BolLX_J5 zd1)X@-pmAvH|C|z%bLHR2k8|D=rB=oF>gXhQU7iOl)C=pIwDj{1h9FmTF3l+t__p2 zWs1S|yEmfCL3d?WKz9(uP<5*%#_%W*4MPqW1hD|p*-#S0Vxe_P4>=OvF200kNThwV z$fV)LHY=Pg9PSdq@%Lv<-@2yJN&6>$NX(+iH2}~MIGFEnmb#o1l*+kL2xg9r%?Z%4 zNNwWPRrU*|m~ByOC66l@ZpC)ciSjj7i&FSGz5JLMkNpb5Hc z>fZs1zB4iC#GRd(Ipn6cprw)GglTNmV`gRT0YS2RDc1U z)$TGbv`B<_U1ZwuGG`+j)WvdAY@Pw}-rR{6#KGghB2G$_O??g)A~3HW3$PJ(PJ>iE z9ZlJPRyVHTg)yXPAV;o!mzl$S%b#t7TtJ*OT`PmJA5laRsBSk52%e{M! z_|YqD7wO0ZRK~ml$)6Os9L59-zH%5y)!7BGh@vA}9l0KB zDeJZA<@6JB#h1=aTKN<>- z)F-ew+4a?H4mF<*qupo;gqZ*s(DxK%;`Szx>Y$NR=`cZb?@NA7Yf;L@C#$+obM}Zx zMVxHCO5%u=w@Hky;YIi*tz8E3v>t;Ui%LeqC-Z#Te%T~WVRQ?DeZ_)sU@<8a0xEBD zT=gtY=jHIJZKLPO#Tf+5tdovY=Xdl%1uQ)UmNWj+p2d>?())4HNKMALoCd4{cQM*E zgT=w(ZTt|723HPi)<}gnogwl}+*AW2Gr6XA{eb)>E(jXe)>FP>?Es!MTQ>%=5Vlf1 zd)u2Jbt@4{WzfLsNJG2qWRdzI@I@jax5lMDl61vw5c{Zl8;WGO*E?V>iUrkM0Ax2H z`A`Nt+aux%>lX%Y1Z)>f#YL;v+Xu_2<6EfCj~ipHU#)id=8ujx=epc^(~$oskWd-B zvyqLz*2sZ7>CK<&hkLOL*5S*yso#u%syK zGaSTxq}taDEDR==EGD`P{l&VNgmUflHb-}nqZ$mLUa5S8u$kzXFbZ6$z-G~OUo3v_6EMNb9WaHuh7@b-P_l@kJh zB+lwX0K}=PMlMv8vL@yD5v^kNY#7f##CfN|hxO8$R>T+t!z>qo1(Bdz{h*WMgCAGv zosT(o7SvpGY4n&QX*6HeYYgoN{GgS^ZYicI4vGcVa6m`CNkyIrS|9Ye16%)QyO@ND zmp|6=93$20O)T?9(g#uwK2aHNRFCtn%47<5k0;6mU=mx89F5%tY|dM-!1;`1xA@_T zC7)mb*5Zz8(X9A|QW6pt=>i0^E$|PM*Ko)7=m890W|6(Q$Dz+f_oxY)x&mtk1YXvC zGz=0K`?+RLCv$5sExl*o+#MBtI%LYpGE*5mbI;Gfv|E7{3ub2BF2rY?+@_uC;aFh? z$Zx#(xXKdnnjKT_i{^kYjV)182YI%D znLQ3ecY=Usi{w*p3Kju456S6Ft>rNl=H5BfQjv;Eo^Qei+3JT=6tTH>rK`H;I4u}< z?wIw_ACDoX&OfzNYHGPXtgkE`O=RKbvbp^?D5c%nDP3$9ZGNPjP>)7N`wR+=KB5BU z2*2-8X77n=PG6UsQ6a>W^`d~-x3D|wtd%)8VtW(Y_=KaVf;-5(C-De!I2KU{dh!#< zs*3Ii69K(yZ;ivaW2;o1n=0bQ+U|cdDux_V3Kl4I1@Udz)aMLChoB5QamnK5aVE6Z zNBUX)5x170Xl#r3lDrtYcW3{68Yf+xYf?+!W0ZcOqvI1Z{vbJ2O@cyT=mZdk}b^E#r^`*60(zKUSsjg3dhC zD~-3Hb5{H^Pa5xWN{Q+kIYxfdUx(I4*$W7}B{}FP-sNc>AnfYr{hYhU)aiown!XN zIK>=(c%+FG>E?R&b;mLb*Pi*Vp~UjSqBEg>IExo6wR0T{IXkj zPZiiMSV3BkJG>c*eoSJ3tKQ+pndKCapv_wtT3sc2UPp^sVnWXV74Q{QI=7yEE~ zrcpfzJ5LT9KqfL3XrJOU2M|&9@KP#pS4l;;;%1jKPJM2X=~!nG!wx0O#aSx9uFJ;y z8z%viu>J-Du*H~2Flkl+RtodkC-l)G-OlN#n-m-&nl44!?sG99ONO@R&75y1kPrvd z>8lH6NQ5F6^6{WEv;`0j1VEZDWCxJ6Nt07>QN;CWlw($n(Ko~@Gb4g`@!t%#7hqsQ zSt7aqX-obO;| zvYE$y{HHsY*_PxP3ZSCYzzwzzDGb$A1q8&k%uVZc_C!%Vu09wSUYJCi-+DKwVU>;s z=#(J+ZwY!9OQmT0_ckG^n*s5j|3{nxo9Mp}O1nA4zrqR;XvbGu4LTdNWw&w$<#Wk= zRjwgI}R!gp$yE$9woT z6^-p^#3P?_)6#c(M>kZ|%nW=7VlpO4TN(oO>Vri5kEg@PV!-avknK-r`m`MCSNlDD z@(QLHD~vJgYIR0&aHR+U4HQgBqbRV+jGn&umej!x;P48Ut`A@i*DcmImfq1~ z&UL!F=@E3qibgekE?*U}tia)=Z!I{R>a8=f8O-7ZPq{k=CN(X+Q8$Wb4Nu||gU3^< zZpI42vV!yf0C5XDb-tplvY#0L;*@JNamQry0y5r{)1Bk`#p1DBV^0M>sd06c} zh^Yzr!tFvXNr*CH?(mN0^LrGDd`w0?U;ItrB)aBW$YvJ?-vD**N0>%@Wa4vN=>Iy607d!c0w zP+NFLusubc!Vc3+>iNx=#n0TymOSEk>7xulSW7wF4uHh6n_yR=>MyV}lyzMP5I#BI zIHS*T6a&uAAqGLzg}zHSLcx;Q7=E7yU!hJwxd0m_23Fi*8&g9yJL|P0H%9pQ&oI8V{ zIAF%(--k^mt;&k@<5p>eXBhrVRD~a4rY8}$CJq>L@vcAZ^HCW_rSbED@_em^1Y^t$ z`DeZ;?cVClNuHVRwSI>CPPAj?V#-thB&G-EQJ+{zY()-lK>6 zsLi>oyPi4_1xgB$>Y)i65)zJPm)CC zIi;PW)(iuMa$V$=J;UE8onU`N1+i5FUP?Sjp$Y>KC85+NraVdGziT^Q;kth`l{u1w zuekvdmxMItc6cGkolg2+x!>X$EB!Qllz%IX^8MyHa2IkfW4OTQz@AE0;W8tYhzS7* zRg_)N`57!&$%@~F^9}Si{G@OPAQYW@{{f)hvc&w#73L(M8pS=rCzAO*73mS%$lz13 z*&nIvd<-DrHk3BNt$%ujD?^GM{iKQcs#!#1K-gu3dHu{CG%4rx`41N=xM?r;Lvvc{52^U#_29VAT^z8w1jOmR)T{~f04hj{8lLQ$d6#d4ivrt_o zMOlL))lt+ITqe*>3y1z0o6k#yU3Dhg5$rCX!;NLbS5yR_Z0AIVI{Nq^!?n8suCB#WsW1vNt1j67YFk1e5pGM;BCbR zHt$uh=>k59iz!#4Au>$|D>A@cd;x=M>@*LQX2~Bp0f8DkFr}rgwm~$~-Ih9L->do; z<3U8uJz26UWQX@j2#;`ZIa$99zW@`mnkdw|hPsDaPdluYb`B6Riiso(E6v~_smK<4 z*b!M&&fwuH&F*{8xfbEF2c`{G>rh>gZ;+?of27`g3!!WZ_Di|ojt;f#S3v)$b!A5f z=JxuaIx7-Cxb#>6cJK@ymLC}UyGeQTAN;l^O|IH2$&5f3Hlc->S87m6*hINK9|fB#jOb zRk?JwhCP@-T2(g2gw*|j31HMZI>#0arxJK%CvwK32~!4ULM`sl)^EYpC@Berp1N(W z|Feh+nWL?5N_iTG^t|&5SJ;=VDgedH0X=Tq9rj->ITv!f9~ft$^=}N`U}+o_Th_$t z28uzA%Z(XGt*DB>M)tHv4JIKghC)5-=`FvswH!|2rtW}|1s4yca7|CudNfoeU3!QT z_tAIVs;C5F+go#w1k{5m7eTEB@X$~B%nS@|ogp>GU$Gf8?vNHeo2JM+n0YUE4}-jm zV#~SMSjE?KoY!zFD6&_dsZp!|$YfL;@j`K|}p%ke{kJG+EX@N9~ZQ}_W%XNe?{ zC*K6~wNO1}kO*7)6if!xfm_Ov`tet~1|-z)Q(huWjkYESWD?ptP#}k%h9bgMnoVpX zmxqhDTYSov>ueK|Y3diNQ&$X*3XA_Q3bNKuWm*`Qe8?HPymIIxL1-jJG`*#pT1L6N zrs~9+-}nUA5f3I+v4^8q6nCGt0$dQhA(D=WB7MTpZhLn_(QhHS+ue{WZ(ptQD{f#{ z0qp@0?3!A~4l<%ew`na! zPT5rS5pMCH-E8X_yWN!|5F7CsSO&>%Dy1;s992XczML68BJpAa3o(A@fJks&(`Ua$_y48fQs(Qjh5QIAza ztzQUWVQjNF6h8-BnB|XUqXaMK>0|$|bsSvYcUs@nU^EX&lfLBg)?Qro1gtUctWq`5 zG#;P*iL0h!RC>4`6L$-yHoKM{d4^-v@>wUhiz)&~8H&Xc2y<(}70X4_@(z~)BCB&` zn$3!vKcD3uJ}U^lB^cRO*v#hXJL)|Y$W08dBX8r?04G4$zW}EayIR7|e0T~5DmVMu z3hbH4{O{|t&~ENE=C8_g+vk;HAxQh#E^WIPOOxo&)T}ew4EP$wwQiYDjSET5334On zgb(B-E9b_O2g7Fzqr4Sg{Ri=kkjSV8NsG0aJQnUE&vBh3n#G?65tPPLMvdtTp?)2{){Q1OzXN>r0W{zx6}UD zvO`;QnDbkMLdP9@g*t5#LIVrKfz8^pg+j*pTQyUb{V1HIxV;zRg4tQfUN}4m$%Xky2i zv&r->umaT?tEvt2f(0>rdV1kzZI7wl1w&>SIQ{l>BjtCP2{G1{6lk6jGgUaQB>BKZ(QqiGqN$fr`=%kizVm&Gf(IbCsJ=Gi$@A{1T9bDLo}nK61ly~9+$=eH-IN;i zdQdtw7aHd3=S@McyzM~`{Gy^r+9j7)69djSf-yJ&-i%rZ4BuJ6Cl>{T`UL9i`I9~Yd8KdX5 zN;$U+g*c$PC^1y2EBfvBQFpMO;u4$;B!9)^p1Sa-;au#9qe!z=^N@yvkir;1LX8dv zfvYWZxFk({G2WJQC4A3;G%Z02kBD<5CoD>O|Ey?wc*{&X116nD`zoMmx3S32F+!UX zH07bvo<)}nnL4sGjlAgu7K&2#0cB|IXj|a*Pt=2J`=5N_jnc>dTT>l`4}=vJGo2Lq zB)tP)c=P)5*IFp=1`j#MK539~t>I$HkrF7F%lVbK_g%B0A(>jrbs8^xW z?}WJv%oh3<*@;mT2UV(yv;LsTq7LL0Fx4TheYSwte!&!~EdA9y+KkcVA}SOQmHJW; zNQc?2Dz8}QxLbOJ2ijS};X~VON)Ooc!hK$U!DF<-Q=PU^g>vMprj!L4K?Jh zsOSoys%>KAWDw9vV#1K_z8crLP8BgCeW&??Xi{c;=iYb%O zYQ>Dd9?990%wQ?wJUSX}QXoZh6xP7Wgo5bVad1fR*(osGmkhWKBvB z?0HX%4Ns9z_28Jc4f74te52bLR~=V#1qHnls#tg8rzEn0VG%@Oe48bFJGkRMjxHEz z*H8W*b=J=YZTBP=GxA<7ngdP?Ewb4TlFbaWTOJLyr&QdgU`6vV#L3OYUu=wmVAU%_H%$~;9f4V z1+5}41w~eI&L>GQ-LTLY+TcHK#Dg2R~V(lu^h$-aEP&|g7NZxFmR9B_Q&7w`I6O~Kd z*>pyggBWQ7hD}ATwQonG9V|dxSD#7~Gw}nItwLl1!jGjEIs!qk(Jfy~{fca>!uo)f z2F6+uK5=nHfh7&3Dl-#Ugc>Ts+*Q7XUP&nf5B!Dd0eQbDh2_cHQ+qZEQ zuYo_T=+s{VH|Mxv0l&ywVt~X}AO_Y_{(n$b3cU2ORQ?tE?O1KsrAbJsByHLhA_>r- zTDMUxV?UzL-uv@Ic4u{_0?6)tIScBEl=g`5$g`wW$aI}6L$=S6GMFF&p=b|}P>nX* zX^}5l;QPfqd4-^Q5Q+q1ai7M=Zz6pYUQVlz`Y!0CdOWhMz^@Ak(Xf)hMwX)uEMV71 z23fOC3U_A`y?EUT=!u1|=`p>C_A_T!cJ_GW!BMf2R|4zE zZck7c>Vp1Lv4eVDbwRr@RU zwcBo|^WZO|x>bu8u+pV}XMdP`EA=5Sk6!h%C=>9IIVMM#aQg#QYCMBoX)``-_BlIN zJJfdix}p99z=;}0dCzyv)8kbwM$(_{mG%oYv}UReY`-9;%FnyaRNjWv{Q@#tNS$X! zbaasoq2?EckGP3ewsQH1pC@CHrG2r)3BdU81LO&vWE;&;Q1Z24HyLkoa35w*3I)Fr zUG;-u&$Jbo=V+JW<4Ws!T+`dZx1CDx9&zJ>q)n6Wey!sg>|Mf{GE$Uf5%{1PTE&Qa zqH~w|Dm;E&9+dWrDeiq5M!9ts((}>YTd}TAJMxA%6x5k87Ha+(v;YwlwWz;yu}_I1 zVYxYe2~y7BGw7I0xW07skUd8b+-G4G-(GNE9K$QjKikmM#1L{AKC{kauylQDxDSqL zfqL?EHhQA&WFZ3XihrPul+GyP<>6urB-WYE-e&y1^Y9oGK~WqC3+1qN7r*!eK><69 zW+R*}a%_=Mu_z|@VStZhNm!ly9yp@Ty!xQ|UiXO=u*26PRuha{(K+vRHJ>St##KD# zreLIsmcd+RY93sUQWgj*j0i#m@Gk?F*&-Jdr(#~}B^<5%vhp@s&*Vf>Fa3B-jR5y= z+W#e7k@}xfx}W_@?pn4g{|~&w)3X~CdT6X1Dpne_qIJ#+O0?PM7s-H3IL-MO;>d_M zJqM~8Y?hkdvGv?f^CA|54^{UW5^dxX$(i8GF(c9uXiK0R<WPYE!`SzF55WlKRBCT9qdk5U+A(10}KIqaj^ zA`=4(K2DoD5}o^F7KJU{9$~C0N~Fh<1p72}=db1@R8l<8$W}CU^F?O=REtN0Zm5_M zg&li;*t03Z+g4Z&x1>uz1`?e|rMSNTXmLI~cY4gn60E#sY5Z~8Sy1qR{&EEqVy;_? zZsQ=9^+fPX0_{!PP;e+F1-{wvJSMQbWSp-S*_EGvz4I375>dbf&Xcee-O{A)aR109 z8k5zqZ8O>njl>L3+JjGh=&$~N`66cxXR>WqMv|s}75ea0n_d7IloS#(+W_9-uwa2i z+&Bs+v0Rx&CeV*#@^S2r6@MfUc*$EH`aj$L~z@C8JChgIRKm~=wL%Y-#*zqnLXdi1EN ztdmeDFVNg8L?dldRwK7Xhqtc{CWJt8mj3exS1{ZxB**GV!SMsWpj2N5)s(v-yO7itP@%v?OMDB zKx-@bRzMVI*NhXifIo-+SWlM-xoOI4+l|#f@d7`n`9v7SDq)Z+yUulR_@wC^_TS>B ztpxFf#rKvo@lXu3mh89+sOw=_e_A9!jnqNbBu!^Q*PB7rip~=nEO*4uj5o2Jg*c%R z3HRNYD4V%+O(jPGR}N#u!a9YhP+|EpF+BQCKm-T@?J1f8gk3LHF8>RYI4l*< z59}IBhu}jGhdD(AFS?P$L+3iu>Tw`YOPhccuSya{RIK8)(AMO6MfQ)LZqXG2E4GBj{x$8<@vsQbrWaf!-1B=S8f9S@Ki+4>N%f z2UvDoa*r=0c&9jK7;{y!_VN44b3+xVUT5)Gk(*POpSI4*qRnbqZY|a7}*vlJ1gy-ln zIOO%Ga2?>h0O3>e6BaJybsEz~Bh|e87Yh%V8N;^#(@I?vSM@9f7ziimOWWE-e5PCK zfA(?nf8oGjtHIMCvmYzu#1PWKDZ|!*YuS@LKr6b5uCL{)eJNm z|8lchR`icA>uV0*lt;71tYLNC5*u2htvM+KU(lA;vBwPa+~?OhQDyW2BrJ^eS##+O zKiag;+MsurQ5-R<~a& zQ3FU1GM=n^xiAB-by#aorkBVJq$bxjSt`Z(f*vy*m%cyPj%i!lMAiCS7+m z?0cTcBBQAf2ca+H^oZDVxe&2Wch3-wuHmz%N(m}mQ^MR zYQ+rn{$tc(me?BrD#+y!TnTS&c@(TJpPWiTlb|{?dXr&#O@2e5{6A1)tuu5Fn3K6A z{U`p4=<8+Ob#RwZd&om%d)jeMgQveq!hmnuQD40ZcxS_5ul&nh{Fwd&1<3%s6+KxH zG^8ypEmgG~EtqJ?@bp_mUXtn0z?u43Ugbfl?r!)mjW95hHik|Os$O&+ba1x_$FB&< zfP6tR$VTbDwTK?DQ9#Vrz6uwB3|Pq9w~UEdg7tEup~tii1aWSemu?X;lbeCYdfPwv zT6hMq@%A=Z3@wMA0_hMot1eod&QK*op#y$vzhcf5((HQHY81)8{ecnYXkw=-Va}Z< zC+20@iFNZ@iwgdjs?0+SeW0LxeNZ5^^9iXDTN*td?GVo4t2-B%G?6Ne_4bWsx6d|i z$i$fuDz_BlO?%FB_?eIgP9OrU_?aybE=|Mq+7Nca=Od-F#ru07%E8a@G|D8tX0!A- zyY2wtn=txYu#ag=a#`WCfY+x0>$L4Gbb5L(&X4l9&OfdZdLE?rsf z87ZHxK#+sW{UuyKlTuRwd0ZjC!dE$~owUI~#@t$?1++lS4T4iF!s)SYD!{<`%}uAO zfr>!GGb@SZ^FDO}lmz|se#U@eCY(Jbw~F+d^gX#s770>ZaW+gx2?_-5_RmshlmbhJ z%8-*`(AQ`H!;pEjGx$7TVaP*qgUQgos7r!13rQ6Ys843eoxMkTakCB-?Rg{s%p*)_ zCUiXub9T}>X)-JaSB${?9f#2xzV+MS)+5dZ9Vxi$qc!%vMSjDdZxo2!!lWq1oI$Y{ zyXGts!MK;9>`wtdura>rzJnQ{*bwmtU$j47dQuwPP9y-6svS zrHoA0YutyUSa)iOjfIZGsd@w*PBu}4TIitx?^R%Xu09+FjQk}yKslZHmX%HvCSQyu zL*h_~Qv!5FqR?Tx4iAqCM`^)wU3>Nx&!h62>qwh_?MO~4dxw;xZ&}}Zph#<#nB?&P z9gbN{v@+{e#?it0YQ|=~Pxs>#D=`_4^P>PRmKf{4a=b8s8lT9)Ggka~I09>rpf&n{ zqeRq5@7`EVIHlGp?j6kzq`>4$eN3n$+{w?iORXJ@0}9Cte`u;~0cSMG4m=Q|s1y$O z;>Nabb(c3YC}vCsF)V!k!{q8)x!yU zNjazpDyXj~c&;iT;Twx7$_V=F89Alo*!YsNoTBGP(o&Eljv&Ko&XpZ-mds(AG`nFI zN8v9C451qiaTm549)whN)6S(}VKXCOC$)-+tzTxQ| z`tL&H5Gc4{A$3!NNK{Dv(aq^xW=awBG6z%sc^r)7;MGIgaPF822C+X81cH4Vy>W~e^Hqhc=3Yq%WpZ0U7yw15GA-8yDI%#9U23(XE&Z1+ zS~O!GNeMS;<0OCs%`@@_dPk$yEqy_5nx3j{U z$~}qK0KbXwuPO%-WV&R~uUk%mS4_Sb6}#Tf9aH|@7p~kYGvQxGCI>y!5=-D-Ab9@& z4^9;bOkuMKo|L@N4}XCwQ2FyZN@Z9D`=l3EXqR(%fecq@5 zTdo@eo4;`LyqiFR?XR0SCtZ}d_j!PYnu(RaE1jF08`PSPU2Th}z`~%18s5hYlt)e% zXt#(KyWaVu(B>rCVX2u9K~~Z=dw{#5(uy*+eN}=NPiAj?B_4bgNImt~cZ`m%Bonr@Wr7vdNswHd|aEhxx33$~^w5hZd zymSDieS*L<$z~!d-Zgv(QsUSh>R z3Jyl|^t_dAx~?UL!S4jV%H)xPexmN43#gqgpoG^K1!Iv8v$ETvTk19H`V#|PWO_G@ zhD69q>>0oDT6_%f;>8h(TFAoGxwlb&u z^wW|=E*`32pr{T~2T=2Uh9^-FFjE;7#&TNcTu?+Hx;l}u$YCYV>2vv9vSC77HE@j6 z^q1M1IMt--z$?^H-;6zO1h#w<3{cmzioNO!dgl{HEk=wMFOjc*1b1Ixqt0=6yIn7S zjYNtIMr(zbpL{cl!yj@794dy2I+$7jo_g|u859Z*>=j5=0sHR>TGv?;SOXk#KQsOb z7FBpJmG1pAii2Ggu}95Gg12+p)(0~kveF-|qPYSdj4Jn8g|r2A-VfhXRu|*P{<^>? zVG^n<{v6TQq1SN{m)*xR2i4+^q!U)Q> zFhKPI(xeI${Eo3l?PBHKdI(&!>OCUN-D%k3Zl*K%&7@%C7Xja@YD*(3D$Q9XtnB;} zuoZ}+{EHRmES;7J7Dq9ViFY8^CIoWhsjM~t$ssTvJto%N^ZxoRYakt@w$N||(oZgc z)9_>j+=z2S;}W9{Jyk}p%QL=6*EF~r*>qg1)*+!75Jh*CEz4x6R39n}!!w5!bVNxJ zTw8P6wXJZV#5$Ln6$^`!0d72hop{b2IWyZMRud1uKunMRdb6z#WK35g*+h$T1wK=6 z8(7;orz;cTR>h+$sCOvKU6o|wB$(!W;ywp23jNlnNDN@yipo%G$_48vZ?xY)s@4@2-tN|_5HO;^H$K^vH*_-I8)HV9 z@#l;+#zjF-yNnYnG3;-Uj!&&V*rqc^9stY^1ukL)ixbH+uma^13(#;=L$7?g<~vo$Uq>-#HHml0TYc z*R2`ruvTVGM%UzMJ!$IbDBRl(hgLtRW*eariIG;bCNOKjxob4l=VlSdt`({7&y7uX zut_;HU{sLLOPJlKeNnawci+C%hf?5t6l*>&$azuaCXrJ--P6i-xc}c@D<0lAMzti5 zVTe<;{Ds>;Z?Gi*;K=tdveO1MaIQq=u#-iB42QeU87${|D*tk)Y!s*fto!8^8GLNS z`21n=n8p$UT$wpkKZwMAo1erA9ZV9Z_rhB`;`kolRvW&1|6fZyE z<(df#gh^0z-!12oU_a35#NEOY*Y4sIq+H8M{UezX*z zpWKgm`OXrPD)!8D=~5p6YFNG|S>=J`bX}@vHKO%XV*(}DbSU>UZ52To6Nla-kMXFL z%>NA(yWI-WUMEmO2_!u^9be4{w%uY1@-f$iDgb*=6rMv<9;rkEL}63EdQ-Le2h~j; zAdj`Rp$La1w{mxXhdCPKG1(GKSf*_x#$DT$a}@5j2g(Epr|Z#CN%*KiXJu(bYY8fn zV?LxE#*a2ofkl~3xperspi`ZS@z!~c7c)y}c-f8%kiH>9_lacHPmB`^(xZ`8=dl9% z_@F3ycGLD3b?guf0(mbpcb)(|>9js<7yXO{?1e{jKU#cNHzkSYm^zPQtqiqPvN!_o zNT|efumn;R;%Z`EK<)_3jJTCWu*cgF^E}CSr|Z42bA8JVTg6zZI}8D(eiJ9W=*R{h zkTSvPNu}bcfBaD*OA_kutiMbchs+kHumq=OMGfqNMwRq>o~9R*_D-|s3FXdWDZ}^n zI!KQ#Je*#EIBwRj$EYU{8MY9h&m}3xWGl&#Yg?ZnT(j5qO7X}PEmEbg&4G>}>d+}t zfro@}1mTEKlyO)FJ@HA#L|;KNIEEPpEhTxtA*@3a=k#$`Mc>xDT~QS8k9#5eFc*2T zGk8=83)YtlX1KFH0(xA3caieREWDwEV?i}o*k%@jnGJ(2(3kb0l(oXuH>0&XV-VRH z-vWFJAL60%oSACc)U#a5bvsemo>L(QhJqfIyB&xx0&+5JV`m5gH=L6QHDvhy-B*wc zdi_==z2=ZiNFc!*q$7{w0s3%2L_&iH?yv}{2&8tUwYT{}#}Ja-HU;VnH=f)IuY8!r zC{Q|5<&~hT-KNUnWVXA|jc!++1skH*y9Zw-`YbM5E(8%7)zzVfT`QSxGi z-KIGrv9pd$}Y|5FSH(UYb#R>u(enJXEU ztXtGQ>(-eVJ;&)^&D;e{5xQP2XZ+5!L`QCm31n$#_3V=Z zf#EAA3q6SJgP+~?8sy&p6h}-CD?yna`c4Rxd;NoEa*u^S{~{NARC@flHnA2oNb>v) zJ)(*wZQO*T5RPppn!i^G-Az5JlX1@2V9DY;XGg&(!v}BA2hG4_i4V<$u>3Zns3iK( zx7SW^`My5Fa{AZ_3JE=_wg>J}4Z@XmcZiG|UHbruNl#vl!5fd^0Eik*P?;%Y#fyJLT3 z^jMb>vLWu2M*yysTdc)eWNuO>G*DI0DiS$N+olq6Y=~ev0TEF=VDyKP#j!%0# z=Lh^@2@8NVl~KQKKszUDbl&+gge);a>u~(n)Tz2xt9;-VP#UE`Gha?V!A=i(bPoD0Y@5UAaau#Liva(Z)4C!(|%Bt8?w|T5R)Dy zE-fz*uOypz0Q2>*NyiHBnx`M54SBk`O41n?ts?PusNj0UOU@Ql`6%>hKlmHF@!u`` zMZb4j2)>OO>0!qWI#~14jzW#nv;ez!g-VPwWQKE+dYvPy(#CDvEhOz7BJqz26S?zq#ZNM30^WZSSlGa zNfWirdfTYs204EbpN7E(STXa&2a+2mk1s)_l&)-h zAwhA`_sjp788r|C+_YNoWAT})6|uARks?iECQ+cCq45qG+*&+F+kYPH&wKVjm1my1 zENpQ5X4Ci;uQ3Lf7uSM^fy`77WE6+{3kKg@w`HW5C^YHI5(-9fh4?B{3SXslhGbNV zjIKe{SEp$_7g0>Eci#cu#P?mnhKQI$H${95x zGhXms_4L7-sGB)Q3k7@@A_yY4Nz^OIEVp}|O9kf=uuZBEr$iM_SMcjli(S>MuC_)^ zN_~VvrE_Wlo|OF4c4^e-75*SrsxetgJEc!)~KJi^n% z*qOJrNCFRir~xn*g_w40?20ZG836c5s~fHargs>7|F;LascA$tLsU4qCUCQ%c)EcX zeT);`WDX%GBePoe#~5m8DUQK@!w#_zbJ)zqu1Uz*ghH4?Q%vPICw7ni>yrL#aG-c- zC%OQzzVFxl$x$TAa4H_z)rpxEEASH6h|!ppbD?Ffm8!S+A~^zvL$4gg>dfWG?)b7; z0y^~B4nvJ-_%}1vwcxRS;1Bu4a9n zi4bA7YX|Tc>1OUG(7h4ZP3S=`+u6#y-gah##VKVuyGAGdi=c31=Ih?otL|_52a!(w z&pRgtbdg|cpd#Esv5u^FQ2IphaDv`3@mPMZtzJmYDaM4e>WpsDjlpH3MvWeU^jQgT2&@%xgY1bDO=PRDcQ1nM z2s=OQMd|qYQQb{@m;@@0`vuWf-5I{5&>((^kYFtG)-uBzl9pKw+FFeJR#$6)q50?u z1;YR`a7SKa2>6ZNU%G57Lc9MGu{H0WE!I{?3u%VTxidf)1*CEPJCxUuBtI3JReq_X zJY91IKjENU5@C>2wv}~AL4zaDfKhBUIo!DM$}SKF53bfK4K($%>KSNdynCy-tMnR; z2EgcAV(AQslG4Nol60(>euoyl}wH%MsY}$~!rz^`3U-$nSLe zJhc#YU%$ULdP|hBo=56FAZ7Oob6@cQ<>j-gcXhx0)Cq1<1F2La^fYeBihvru8Q_$S zn#VsF{@=a+b9M7J0V$Qpmm(7c({peoZoj;=@n=4IA+50$kdD=HrvddZu38Pxhnnqm z1Ls?!$@LBayAtN;hB%%Sv>>^F{vLP1-fwM`Yk7417og!MGE~G};eCQ%7Er8Gsv5Qm zSe~lQQ?!x?5UtMzf4ZwI_kkhq$Jk_xn0P8^XRQaOeZ1RO8MQYFYM!eKf>N)%v)3H! z)kXXE4*Ac|FTi+cM+boB`O7+*J^03qs%!`Yl3aP zGv%WaO+V&J-sm+AfsE-4#buq!3+}ux3uKxxGRI@mB+VWNha(lI0=GN#MsU_*Vnlxe zbL|?dvT0d*8ENNQwAZ9CtUB(ukIB|67GSH)-wT2VTw&!LhQRnl4j@PJIBz!tXb&Em z@3A3UVgB06Pb0#Ijx!8MGn%Kxmut*`t&cN2rUudiBe?>U;MsSx^gUJxR&friyny5k z$E_xq@(cN*(I0yu#oJtyW?`Z4@-Mk8Mi;F9*G`d|HazMgcN8TTu}N?<6EOTZJJ3DSx$h4iFaWLTZ<5t{-9ud zZEEeT%*^>^qxmzVY=(ILpPCWT4X}Ik9O7vKnvwhf3|Y{C&X~!SGSuQq6IRnSE_DwN z%5?MHqAgAvx)yOw@CqG!!B~_h$CzwqA0wR*klL1|RO@KBF_fN3QJ^}U)^9ga?u;D9 zim^w;ixaZusp%vY2}DwN(pBCy6~kr~x6B*MfYUOh+tJ&vDoncy$q;%-idcZ^$?&df z+D40exxj6`Y;pNF>d%6opz*;0ZVH0XJf2xFct{T++4z(=08vrEJ;SQtDp`$y(}k(Ozlg}+{%1HKl)_Z7$bkLek@j@uwjV@aO_mJ2V)0ByAeP9~t= zi=8TNBmGVjKKJNMRUBpqL?t?qAtKROM3WGI`aoEY`N;hMir!lxikq#5dqH?AR(Q*+ zWJY}%hBf0YiYCR?dOM>%6seq_nWJ+fRAcNbZ_lWrHF|2ywA){|48hVE$6iExiCHiK zkVl}kZ9HG_XU1G;Pp)L=4OhHb)uH}sk*sjBEA>|c`6uxy)K?q$Rz$4XS1$Yk@a_$z ze#c!&0y5PTo0{$CP8#d%+9LG}Z(6|6hS)rvd4JYpmk|keMG+)8q8 zmio+*>o*AedWmUve0_}E$XNhWWk8vxsNwJG0@in8Lc6ZOj8j_%#Ft=y+s7C-Xz$LB zC>BZUNmqu6ar1Aqi;I$JJ8@AEJd93YyU)EXORNL0?6xTg1AT|7L+l_Tc-9G_Aq@8h zhzkfEQ^N9-?1-*DbG6T@rZDE=QFBE9+vL>R<@@*uNk!ViHm<}S<%opCprGl)lcK+# z>++x>eY|bZ@e1HX95_g83|m+;6ZuyIi9oIddIYe{gSTy4$_JXr(T^-P96m^4i?A^? zlM*K3JT-1n?)h)xQI{=nc@TU>gZSSTNqzYR`;SNP>!@9U<`sVMGsXB1Ec~rEz+JXh zY{MP3gS0K-!NMfCDM^Ggs7Rar$1ffbJAf#t zKHZWM@k~Yf7%PX_C7D_mst5qH``&ThdVUh^0ovX6f3z~Fe^la-MsWhL&0<%(xw?Y@ zh3!-Hx_5%i1dzZph_#ow)cd_Xzmcomu_u4y$;|=|ra6$U*a)U)UyNrD5m}}Kwt@vF zX+v}wmm)_V?6-p zo6u+5@U(&3tk9VRsF%(LnAhwsy|qCTvmRzh$X-f;#Bskh$pJ+)GlR<()j zcKbdWxI8VgYFeU{#HwPyXcJUlQ?H5y_Meu5>u$Ct+T|F&yqOBOfWCCiVFe^R((86^9s3WBGBU?(VU}Y@Cu=|P$X_ag_H#OM|vRk_g4}? z80Wc&I%^jdTi}HhohQmzUZ7@&S%wk5b+;Q(K{cNVL@TuPc+aPG{GJcWdvQ|fqsy03 zhC<^Beg`L-qf!8V1tNL_Wf7=g!@>^=s9B3x~sw1A%ckf^g?+$lHJ``gHfQ_9ShfgZLI-@VFPo*hW$ zpJMnAxcAgt&4NfoUudi%Ho{`7jdD9XJNg@-N?1Z+)%jqZ{TadxYe%1&9}NhTfwZ1h z219DII?(r!V|*OBFM5~!d2h{=QYE3z&C&u*xV%y#lD?7?Eg5%21W|0Q-y8kwnPB%C z&!Q!gneB@ho2WoD-GIEi!ukPJ#cRWPB73;wyP}2*TwCaKsy27;&r@Lh7iLlWe8q&w zQ@VsDAzf|JQrP3lC~fOk|CDb1t9c^&-FDdy2<*G2|6P+)BMgEC+Y~B}U?y0i$V5r% z%`dVSb#Ob1@mccXIxG^V+5j&ds zxV-=VWJN_tx?MF3Q6hgK`tcPCeJN!0y>?3Y>0MS`WsO=!k2q6a9*d(Dsk$=%*={vc z#**j|#c!Ewu*I&|jX1Fj3Uy0L+p8Mt)9AJ6vv9UO%dCFM5#IHoyae71V60hl@ zAR4T0!mq#x;U)(qpOOs#@s}3j}M$`q;*7|Ab!D*a`uncnAjMDs1Q*rT<4(Py#^^oA%P4B%w!CbrFR&!~-kd?^`$+ejC1@Wf-`|IL9Wd zt1(lMI0J^6bOHbF|L$*x?JAo~3}Kvi>Y=4*<+kt;G=;hexbu*+PvK4dUG}kXnFoPT z3O74vc&@&k249>55zYo(ecp;%nX7BapE!~HZm}8*3E207ub6Q&X^OmsNKvpaj-ID_ zn`hApn6~f4sCGvr_k88W!sFB?)G^wUv5x8dq9C zP_RnhUBk+wUA(Hg6HxY^w=SgtVJF$Fo>!$MRhHpxQGUy{?izWnVQqcf30=3T5aejO zpORp&(11l2A>P7{(Id?enN0*2;4k7!WP^DH)SxK53F(i^<6o@2`IwA<$k!Y6!v!i7 z+OyyMz~EUrfxQC;jb=JXs;dniyF3Y0amiEySR18qA+AL-hChp?L4Gj_grD?2mwJfeRrcW+X}+CSVuxSr?^?C#HE zm;11Brc2si(bkHL|r*WKs|P|CPNIGwo~ug0XUbT@)Jv$_Q@wq}il{whF*- zfv0B~Vs+W~t!bV5TX5yH(bRYnpw{a>#@DCGkfA5?kqcQa%HJguwTjTxBa}xD^v)bLp6OmwMZFBUj&Pjy z{i8>nYnMr)%0n~{(Nrzrj{Mo}DsYZ1c=r9fR(MSS@dglC3a2q2Y@9N#)Qhi-Y71-U zMWK~^%H_5 z4I2^*cbXT4)JO7O5gU4{;5BYc0*Mq!8QT={!z&BZlKX|!8ViceTYp_&8E7YE&1jAZ za>QBwuMdjuJ&e1#&9&;e)K+TrRd=^GUttbh73Nc}pTs+RKh5A=$n!b68*9V|AEnR$ zt#0N;h3bc#Dg55&MZQ;Gem7Z)L)HA^O+A1+zubuC$u#Y^TbelPKM_6{=+&lH3QH{T zclDNi#WsDzGH+?hg+opYC&oqT$W;d@6b{Wnr^ze+MZ^=o8qBQ-?=|6P9a*w6dEJhA zO>cf&>w07>h}Ws6vJFJQU6RsX>zfFJR-}-2FRQ6?5cDA#9n;tT`O;|urozg!ls2@$ zl4Q2_zVt6NA?{jY`|y1xJl{Cu#621Z(d%X4n(@XDO$O44=G}*AW^fMx3rpg#$Q`;(Ov6Br2W_!udoH)3F0c40a|qK3dJF*W%-0_1 zCZSg%i>u5pWq>>wVTQChQClKA#yq9|XB~+;Z8v`71*!nPbo>yoN<D$d{*M-B_sH^R;{AdHqGW;rEWsn z5y`d*DJQ>Ei3uYu4JR?hz|3P(KXnKa&n`PITNe5NRNB|BA1@#IQP?ZoW5|ib3A&IL zId{)e+VCqJQ6?Kqg8m$=m-u&b>Kys@3N14 zjo&s8EXslbTla26sXz*!D7vOBYLL;FF&c%#-&JcB$puVf;b;Bs2SEEkapTmyh#GlG3g&O9C<{ZM^9fjKIyy(U#^4}Zwaq#b7kJjIR6q2+eQ@gDR_N4R!}GtmK) z9JZJ=+5Ue&8MT!C=ZrwIz|SEiP>#qi^w5S0)7|Z?!#kD%G5g><^O%|=*r;|b|6?IC z^)&QWp=sB~@q}{`m&prE=9qJkAzr5qQy{?db;p1B1$?z0rbutOJ?RXn4qKw0wo8p6 zaU$iC^6;k&vaAKZ+hU{E4&hh{JX^$);O@4N>a+roQlkT;%s)i6aY0YXZQZSf|9C)} zxCJ4zMaOFwI^Is86jZzp)yk2e*wS{>BF-xM7;|noytJ=B>VAJh%E1I#;@}{Ir@I6> zD16gDUL_Bh4t(wni)zURiM_o(1qLZT6Bsv3|F_va%U*Bw;pmYr^|p^eU3N}$t+B~X zY(vk)Fn-AufIFR`7(4ixClvn~D6A60kJIPadJgzl*m6r>2_8XC7qMGKWuTU3Iv1#T zWU(F%{`IFWP9G_Xs|94r>=?rF@X7DlCr^xuyf}O z-^JhKKtivj+y=vKey)@FiPcdQ?@^<3{#Wbxd2>EJ)3X8oxdO)DF=JQzwr@-n$^?*z zVm)Tv`Zox%>x|u)F_2p{<=c;m+u^pxS?XpipUac4Wi{_Jd9LsgRyE*`D^W88 z{|M>yJNGbtwTM|QOh4vXAw@(Z{^znJ=s^LcbBc+^h!cJU*Doi^4;L_^a+5-5ImuJ6kdkEs+pQb} zVP!iyOsQ08s^Hk$Zy!9krN|&t827^uQQN4p?HZx7(QzQ-zQ)~G?itMI#@W#a$wnG1 zTbs;;jL~GL_$kRdv9KaQi99|EcY0wQgh*s7nfL&E_=1ToUJ5z;xOC#_9ADCX;%ztc z5dNM!NefTj@WmC9bF=OiLh)eqqmz}v{__B^VTPzX9^qk7O)G#GsMNnOa`Ky!cXJ?2 z9Q92s&t#5#`5a6J8w8qromkVzp#{o)0f+}kB^JTC1^L?_4M}+Tt};eE`_5ZSrBL&e zE%m@J&hLL1n!gQ=oqJ`sxq)6Fq1-oy26V%o@Xgz(m0g|Kr)?HvA%k(0>X;pu#yIb6{UMhHVRO-f zN9wmbcfX=@Uz8~zXmB2KZILMfzjdmpM?n1)W3)&6fix|YFXC<^|6x5ACp6K_cLY80 zgO9rB1}={U|J@vNkzDw&HUYqEW1%s|wyK#{$Oe50ZlEEWvEj%A%W>H#B8Mt<_av_# zzshsa)vod-Lx<}otQ$JKd|FrJPE?S0y1uM{}-ZWQf2P){eN z1_GEOx9>_?>^^k1E3}siwJYx4%oLGr9p|B%HQE3fg)NIH=Sb6p6%&8@)bgfG#PC!< zUGbi?s>WqSXXp)ZkiDl-5P}Zh8|kE)*6nr%G7yf1w{Lv!C(h=q-w$TL8Dnfo6=8fmQ48__@uU zboypr$Yk8Fo%MG|^Pyc2UX`0En~#P)4MJ6|08K!$zl>QB8^>s}k4__d(r*KKcvb$a zZf-S9PeuCgsqwKrW}mA15lfAr3k*3*=G4&4`9^*t;G5|BgijNmj{5Lw0NJx04(pJv zsQg8gKqZ`S57MeX0EZ|L^wS+StCK}!2ZC)tH$SvB$^4XmCzVy*Rgp{55zaJm7;39% zhE#wTv-M);83FwC%NgP`yF0Fi)mZYA5bi<`npu2H{=BU75STSKX+I?x&Ar#m+DTiv zpQrWw2GlUS&|$tpfl$fx*CyveXSJ~wF?Z1pwl}iv6H6&g!4nzD49VBZvqb!4Q#E5 zD(n;QwH=qhoB0^}5%iY{pN!CO(z7B|a-{nd1!MdiyS|?C6aHvE4ByFm2K4%BvikwT z2tFHlW4h>JEAp~qW^6oLFCZg3@dH9xfwrMHGB+>78l;I(Yg{+)3q5V;enR;j64-Hm z)z=vd$)+Cu0>;1JE1P)ZjgK2Vf9)<{Shp*sA{C6yZe!a$4L=g-y;y#CPFyyKH!O@G zH-B;E6zaYj|8Ke5d(U_+x3EpUY;mfqk%R~bSrBpk6|8^4S+~>gF9B;+F|P?RWx}mZ zaOp;m3!{2v-jvVM*bplU5BfnPd?JezpNW?(R;Y<6?N^`Zs2omc z?a50`@uTBrK1UX0cD+2pyVZv62m#vf2XZW3WbR}p{cB;EMAcT7q?zjl?6p|t%~x)v z7Ds0c7R-ZH)ON9HTE5D(rnw`>3=G-8LxKk?vLteBuXMX2tWz6vTru;GW_baRou+aQ z4icG}yhHJkt=@O%%^cwB3am5_J3qRf)laR-DaZ9S#l4f>uwSe;@5;4khs;znJW-Hq zw2*~!JY(`m`JWi^3dnef{>w!inZ-sUomnLYZ%@@+ge|J@i-wiq zX695?6egGx&J>u|Ig#$#_Kn{(H8qTx8PC5?`GcOwwtQ8P{d?oHV)H(e3PB3T!!AI> z1PI>Qj(0vbjFwG!b&KF>c_i5DO!6lQ21o6zycPwL`53#g- z0P*GQm^IJ~Vjsw6_aVvp!@j$+cL%eh4Yb2kSNeL8HRR&Gyter%zxg`r2;cD4Pg}}p zUscE}2p1U=4=X1omow&>ng79lZzZiDcV+f^3Y@RF&OS-OX&*`v6JamO25dQ=hs3>I z4jgFvdtgqpi;fAL*&%ke0TbAmdxR zlQK_CtW#Ofa1cIV!ZY!rdF|wIG7c%wX9k3I4^@S?7mobCh7mY#5MXB7%3^k_*QV&z z%mJ(#A<^u@yyH2)g`%3H#MGP(u?=}`(ymiZ36i22v>QYuntdneN1#00s7T0f^2aGe z+j8r-Wf`S}BI0cCjm!5a13=1o{159c0hBH=Drj@<`CFdHzu;SG9cb`U&lO?uRk5r{ zN1TSio1OW0$4;a`+#Uh*o-4K^0@`9ItwK*OQciy$GYLzPr5;ej+9e*!4OtX;-z00y zi;iGhpoCAj_eO}YlYagE1;S3dKWwsWTklL=4X`j*e?+X0Yi(CQx_$S~*r>u0NLS8Y zQFm=)3!-Iv6_Scvt9c^I@JqyZBM-D4FMt51w7n4)iU*WmgZ}h3rehEPf%62u!c1y= z0BwH(PhH-|i@%gSRk$n=G8wx?JAJ>4BGlz`^2uXX(RW(!Q4j$m3V$H!H5F+~2{vJb zSCer}b8O_PE$XstRrKf0)2)6#MqMOh%jEEnjARIiJ2sSH3%q;&+;DCd1KP|DHro?@ zuciLjSkzkU|u;=gE|yFw48kdGE~?TkJV6?;#$?O|U#21?7ktB8J7R8Td4r1uIMzzQKKc|l~3DmF$CS27}BV#W|yE-4_fy{~G)MDte zu1KY?)r}qP0k-O1P^hYmknQ=l8{nDewBsy3hHD24HDgJAn(pU9E=1?6{WZ(-e@8iP zO;MhK7R^btzd2|sSu6h^T%QFgL*!!RC;#6TtQANmk~~nT2;%G6^d3++;Fdv-*!Bb5 z0V%0(Zeb9&@8eijg3MA#QOm^jC=Ae;l{vJ7v852bry((R2I%S)6bTEdM>cLu7R}qv z!4>jU=)5V#-KTRU`8btKKub|8^R$~z_Mniz);y7d=Af}r0lXOh7QCG40>}|;?ka;M zEMt;N-@*>Wyy6nVZjEvddJ=APH|Dy3TtiZq%QULu;I^*>;8`{b6xcBMO!+GT+64?` zJjNK(CB$xSSURoK(CTBfJ!5mA%HD*Xbfj39XY}iVmGP?t452lTD^NK9^g62EiI_tf zuJWNtI4sA1$5f=9sJKs^^?vj{&Bu_Hd%mn6lK&mh2cRAvqZTIXpkmwNn0?`}dORQ; zsv!x(jjqS4NAA;#g*Dty9UT2Su@znl(D%Qr>~}EqKNmlaHVNF9Z%yscg>~ z63-B(>edkTc(26>MDnS-lohcWx$lSzqmW!8vo zsE+EY|G)aX4wuRL0NiwI&&J|_*OHr3ie=TU-^4m5I9kXx`);}=Nu4SGiY&*YB9ODu+kLTN}i#4 z1hWm6Upcg*&u+UkDqKV>({AGktN2i^;$rRF^*~QmgEhz$A{qd!ad6kk`_FA=Eb8J4636TI?;tWfccr2;5g*#0C!6_u z4g{PrnqWDa#>Owst&LYUpc|TqHols7o$AqmM#C00pYGU%3q%z)@Q<}BeRy78AEPhem^JPZ4nAs^GuFO zwFrhx=(ueEE#4zIoq@wY+BJ%P6EhzB=&fgxe=o2_6YAkoU~yA+=~$i)HOPqLorv60 zn&><2#hxbE*#ro{qZJx)>FMBwJy(FJFJmFS0;Mh_i>=#>oHCb;#964J z)!ipdf#DYzr!?7YsAl=l1BzH*6>a7v;AK8y#s+2wu>1ZsR#oy@FNx4iVUk!tos2vH z2j8?|{&MgsT5gj};plxl7w6+$bBY{EJY^`&8d)mJU1w-C(nmo~s7md1@+D8cMsJ1o zAzQkPLBC{E3LIf!2pcAg7+;-NqkI&K_hGf_FqRhDzjGnzNLloUZWv0NefYv6vDTjD zHb~Ih`&p^m5Mg`E#RprMNt7 z5-mK#`=y_gly=I@0~V29M<@$O-alw3>|iiN|Hfw+#Lm79xl)h8dYnO*b3qSr0i-;h zZ>d`tLi0B#f9P!<)QYQAVIY;~5|D^2QT4z&TNfeB_6EIdn=1OBa zJAnZ?U*Sj0`MB6>R>iSALmld!{7I_`MnABNsZ9xGhwv)I48weE1a3~g*1L~((qPrw zl$NJ2*(;B}*@9Bo<&PSL|KKhz;kQb8?UX%93Ef+->TnK42@c1btr%qqZ(MR4)Q2+m zl!ZfgL7G38nugu>bDfXE54e{I~L|BN{vhx-pPOw z&fn@2iGqDqfEm@P1GDCPRp~qu+dgtru0ZJ0_H*ZzAv&CSi(<64sS366ApYi#5w$Y4 zs$|$?2K=$t2j0uUD}akDf1#K&Z#`GKDsk^kbXV$0Fy4NJjkxqn-HM--J_4W_|hX}p6PmbN>DpD>iRx_3YJbwB@2o$n0BggL<(!P{qTxBvJ9zX^VCuDi75KV1mTfQJ&F<3e-kta$^vHfL zfRIWyM$r+AgEdKvkSH0^TGdt}nAK26Xny6-4C)jRIVLebkaP^ZEw43H>=Po7z704o zn}DV}h2~I`K2D>_LRRqRd`&j{@4D2|16!GqEpajfur_vfU=Ns5&M# zJO+&2Isr;)+)74&5HpC#8N~kxn$!rhj6Ynr396tEl7WGJ%~W6j9Vv`P-Xq{vd?*U6 zbz-~i*r>B-1E>4OCc-{^?EQr*sSZt`4-7en7;8mG7j0tx#1oQ3;R?Vr8aWO&>b7tQ z##aed7h#)FrmN4Thu|7SGG@|la-xP4@VDUsK$xkDBD*; z3z9`fTu07AYW&&<84BPmvpAxXqPO4f&y!?C)mhq|b(d|v$xEF$k(d4Q0ApNo33GOx z&Xw}7Pd5_t?5%wdhZquadR7S0^7fBzf2heC6!kqxjsVx5K+h(36;d#{76?Qq1zY9T zNy#lU9}Pydl~p(uMnyd(Ie5^TKeSD_yts`SjE#@qbmYR&-wF~{2Q7LHO}?4tSNzGF zGvBr!;s{Pcu;yHSPEBJ7rZ~&;^%hBCnn)qQ$1`zzZTTV54Lo+^M#JL17yvoI;*nSL~r+U6_E;rxEUo0u$s6tn($N{zLaulGF}#cT+$ZsBS% zJtaODwiO@0R=Ab#7@)PPs?HyA7B>1z-j&L}0bP|d9b+V*lt!r;n{1o)LMu@`1)gpF z&4~DHA_YhgHc(>dbJ1P+Na*U1NrY+i8CR_&KSxD_gHGlLlqsv8C4&~0gs4g*gOr)^ zn>Sfp1Ou}-S7*m-W3l5G!%{e`0)>!O(*F7K(3#%xo_cKbIQ!Lf@0JR5yjkP#5n0Ji zk2&qpLze6AbqEU_Os#wTAcOs_K$+L!+$b0nL?^QYD(R|@C?3^70 z|G!9IHh-&WakNl!7xqy&9~h7oe!e-KzOFmNl7>FAj8t%qQ7{be5+@l+rkeVS#5x+4 zsw#G|$n9g~6_(tH3%pfun`KkJb5NgOy18^_`U%aY47l{`F)*kYMGegz)%TUNkn9NPgu;^YQA#P7Z(866Af z#)d74o0=-4SOuRoB#%GI@11cqsFtx9QlR<&L^Og~F1p5bV_ZE-c)i{#4QJ+j;5si6 z%*V*Od81;7_;6X5D4Pfs(qFxOvnFYGW0UtJeMUNRS`vUdT%B#S@px)RLJK83vNAVkUs~h5_?Fm+o1-4J`VrNS=(@5ekmVS%z zWteIQsZ18mJ5xQ_0UCC~DY+z^7{3UA3P-7atA-OQ0!8QTj#9p>8)R)s#sP(d=!0ia za=&{SLLW`#E|LEuItoWaN-oCXAKz=VkUe$?1xXL>T(92I=%qdJ%@8^B;tOp#FkO!P zhwe$n=4@1Xv!SJ_D1Ep2QNqr@1W=Yr$-JUGk5Qq8KSnaS6f(5Fx+X-|TZJQqAJ1@ZxWM`Q&M}{~f6noJx z4jHtQDYgUkvkgW=1!@f+0j79nW-m!gn`v|Tr)msT&+>c^G{UP_ama_aL!8N(PSmBg zN+(W*X%_~s6H}FJZ@CKm+)*qtzO;NTzjO?savRWrC?UyCB#+7n44S0>s^l5XV zXimlq64R4pTpaUtcQ`n*lDk4%KX4B4Ry?YU#ifHbGlTwwY7QJ89%2`)A8Sla4sJCD z0lod80~7Peu||>_!y0SQm`3gT8EOkJMd4J@gK&V(0D-jP`C{m6Y?R`4R^QdJ1@IWV z8>yb)_~ol9VuN*)8x3#;IwU?l8TZS*OK|k^q+X;#JVzK!LBTq36@kX50ud; zW$c$E-V}_iV2?qXnI*INh$AyD&3tT(=b7edDuAwIp5-)2yS3w50SnXGu&wFQvTlZg z3@l?c>aRQQ1;iHl^ zJ>M0w#X;5stUgfU^qKG(V}O0gsAkiON}!Xv0N{eu1(gt)V4<7?C;(i!igi!?tdyuB zIetDOXp_L(J*8JXz=jpicJ5uNX@o=p6@>1qWa~t8(`hjjl~!d*QE33U<}B15Vh9;=#!6tuY1`xyKYeFJQ83 zVTZqs)$k;A0#owPXb>hR7oiRRXY)PEGy1896D#SnZSPP;MpwS~NJ4JAUJ1VK;S6%1 zRoaXl6Wt;kQ*m`J`Tu%@)#`JebIL-T?r&bExnjmw?_u@%*E-Cbz4(uKLkSrolhT8V z6+a*6xUR2ZuVlCM1|{}KUxt0=cP|*CeI`X2%=rFgrgYk^ZA6nezS_5@X!}2J57ryNPb%R`@0fI9thkj8fw>F0)2K2+J&Rc z|G(sdjZ8natA#`d43b37($0^KA*BFFNEMhdC6b*i z)|2Pti9*N9NC9no0B?wPP8(6hV~ecTYhPP~7vIF5qcx;+Q>}S+_aSgRCdZF92-j}H z!1Tca0WV^Ob41B&%u-gEW$Sanb%Vtj2j8;*=UMWIPC}M;)%5(Sat?_#qt&ZDAN3Fz z7qyUKX}P`cd+JG0wZfY|8RG#_7}c3?RZP`5!Z{QQKdFd9BM5WQEH=b4#<%hzey@>7-4kOgrwy=E-xLWt0kZr#%k(n{lcFsp!zxzA+HN%yF1`T8nZvbpO zx^74K2?-KgUnvU2KmnBS&3%BXz?5)wm*sb*cqxo_$UQI|304-y3%20vw(QlHAi>r> zc(Su|Q%P9+FjGnv*{wd}DkUxRW2 zueGc%N88#5C2di<5aK5EKK5jf7gW`Y+iX2Q0W|F$5}o%K{{KHuWKII!$+Fx{+;ceH zU*sAWZ0e{qN7MyL!E_6t3 zJGGh1wb~O&57S*}tKix1JAz|#1CmkBn!s;_gR3N>|0u>pdfau2nqZ*s=EdHGA$i%p z@(ucZ6Ne&`U9oUAIE*io!DiOn8?ZZZybo}!vf~6(YqGUxl z3DN~C=1*ft2#HzVf6Rz9pZ3v9aCBmt4C?}uElDC>Pox{<2;|b`?YK(T^@yVQ2t}L# z_g~LeeYt?Z>pqjniFzs8d>?vYyk@=}@V6eo=tg9?!Us*WkkPqMBiebD8HQV*&G0PQ z7ei)C34efqTgQh~B4+yI`4sgJB6d5b1jG%NK3KpxYbQ=+UL<}aEuP+S6+={t&B@GPn zg=y(y0*P4aqjPFV{{AX$~_wK>x7PXE8?V;>OQ|SqBb#4RAsOP_i@+yuKVW^{X z>qJ@AMbAJvLaBe5FwVg(!pflb3AAb>+37m0w}sb;$wITw)6&+ERfJzF)*NXVU>KYj zo4C=o9OojyQ@`F8YFm#h0`&EykB}v&O5#<5!0NFCIwYb@-@>0aw@wGr@`jN#k!!zx zF3UZDe3*EzPsRI@0rGjHfMewLUkL&p4+Sdq=&#&)~7#OGj(IAuJ zJ?)+k1F1}qLPR~!^hd}PJY#Vpp@`y9%1iv8#DoJ`@G-k6Fy*G8Ry9FD=<`K_;7#BG!NC3w~JMQK_=?5C3PZ6YbrG z%8Rs{X)QR!M$(y-GEpu5*PFBsf+1wDUK`f5hAaX0vCMBk4sE+$DVun?1{}4Xdw&tw z?!>;C*ZOseG+Gh}aW-5DEhqv!R<7_}8L8gBVO{My<1DbYiA zE82rJJgU-$c4wQ5=zMuK1nqZ`i><6t+5fE?{Lw#!1_=7Ocl5%Z7$4lNaFS`PW7|D4 z^H!`{C1Q>nafYP~W_5VC)}L(pxA@UZ&wsyQ8S>U|$L*E_{}K*3lF9zRLHuf`c3)GY zlb;=ERaMbYK1mw|vQqvA&{-Eq8LGm~0>EBgPJrj&#>mmy9(6=O2Bah&i$h2H=RES< z{ALaTKLKf?xet~E?EX^m#84q=8MukBrFCY#Y6vw*rV7NA%{lLMDMT2^J2ZxhXuaJ> zjmC9)aipI3E3}}aA3vi?1=a?lSU^_|&0v6a3Ubd6m4+o`0){U@o`?ZuPCN*~$#D!o zh>UV2t~c2e>UKaA5!=m-tO~@jaK*KP{mOSkt~ByU>wEFIeMJ_eNcd6t%c(Yed~RoSbgjvI z9ytCo0xE9J*1&AnZ1bg_CAf1|uCIna?-#J(`4 z3^Du22F(WfE3FbpXSwX@NJxv#zJOa@h_aoYmL`l~Mr-Lfq=>Y`L|3{g8==&^02@9& zcM!jk_L6(lC;QFZXyn0L3aWl}un-O^W@|0N@7z8&#B|*Gt zY`aCBv>(>#{82)7mYusVIv^}gs3LGh9_XVU)}Zuhx85d`fm6BX4893QsKdE-K}aLi)OOragUXs(rEg!`TQg2J@d3owxgB*~PA1khV zsWghS+%M(G3uqc%kIlmF4wJC4Y>A+G1H)G(1F+nZ1Pnx<4llO z8))<2cGRgO!X4ng3!IRe#!#JUfD;-*7ehMG(i+%G^9`Jxf7|vpa>JbB+K-)Qr{*j)-cJWu*E}lRFV3)*GRgIX3R0xgC?H zHxmn533Ph-K~c2G;5b<)C<)B)4Rl_PLu3+XX(&(viKP6m6+L$;<8|;iPOfHmESCh{ z0+@(P(ViE9+KJv3*IH28v<@nEZpMze=mUc@pEFJxMQWPyF5$E*_3Q6rmYRhl6{frL z2~y5xK;%VkxaS*(3fqQ)R6&YWnL&WF_h!U*idF8i1(M5ydg4}w z$O2Ujv&0M5Nf75Ftl0hOZ7xNtGL?cd!=tEI$;5lhdVY)w&DW^^3dE04(8l^!e6ZPF zF_N^U#nngabc?Dkx-7D0r*Se&iYY@fMN(IF{HJn14>U)#6QKLPcg%zbCu1;%ITows zhl~xhr%`jP_=j;Y`ujDXztOK^vm5Yvh`!pMUMy zSX|v3Auy~Vk&;?`j2zWl?>={E87Ve{d()!8$k)i3OI_&|=T(2OS)bOPzA~kZ(NbgP zw#K1Dma}hYYBB}IwKB571hu-tGH%A`harkKy$L`am(5@E;lTtpCMindI=j6dgp}&) zF}ADh@}U?ql+a*U3a!Z_+RlA0DAWOOS4p);p9|qZm>r%H>$9Yzedj_BMSi`oEpXle zgu0#kc++t{1{38;Njo2s1j z8Nn+aS~T+py*a}I#(m2Ht*7XvFn3VpX22Zk0?fL12eSaO(gtwqj>)NtAs5qqMnt;dct48 zqXwu8Hy;OV^csnzC@e5}Lw&`XRc*>fFCQMx&8M@+62E^46LR{sgI8FR(CjwgoCusz zRUtgNeF&Jp75noA3c_{vpRG46y`&%s&m*%c4B%}1*Aq%TAv?n?M0hisLU+4jz%%fT z_G4iaCniW$Sapg%K|-GBYi}S?@(I{_1ru@TbNhhoL<$_Rb+H@hx-D}Jjrc~Z(FJ*p zwpzV(qkC^*&;$INC>A&g)!AUn4R|Z0MemBE3ohqpIse4(+O9L!Tf2vZfQI4%t2gen z*0s7$-*IB1EEVBExymbQ4+2M9I)rH zFG^Q10Bbgmf>PW%>G$;tBBp5J#47`k^(g-xCwewPPm%%`-A?A^-S=`G=r_<}TZ4X1EVnm2cph`E+8HeEL65bjjGWrJ=6LxDzNnU16 zFqkYRRYB7D)M0DRNZ^d7c>F_vK7sO16S=A7n|f~p09pD7NPa?qxZRNyY(E|26?8F` zVMzOVCJQ4VTBl$*WYcmn1-uM{Ie45}2d*Mn7;7+JxqP+nOIP9+Nzj=OG z3%0yV9OF1Y0XVqoXI?zx+Rz#e1gg=`spYTQ3XdT!MM_RFGp%s(nf~+B_I@$b9>q=X{x z5ZPU#PvnluaSZIDBAWxtAXKB-cTQRtA36<;=*C(88$b7BRnWkeNz87-lv<^`2G8U= zbF}9OGJZfXEGA&;5p~4}O?Xe8y`hZmQm2O-jE{5N``Bfm6Nd~v-78xS29+t@kOoN{ z?H3k@YKp_3n5mT?4XucOL^x!S?3PR69kCNBDsG5jEa$s!6$|eg4$DcHjM; zsQ?jkDu*pH|46f`-$uaAE>5QX5jhUwQ=VB;zmph)4s;t5{%QKD&9x)zgln|rw(h`= zpirA&A#0zk>|!7c{H06tju`8S2-%$N6egH{0x3YxhjT{#$WX_e%LMtP1#2w2?sJ>P zw!)4w#A`Io_}!ZXOwM;)%nobn*+I-$2@>5Z77_!+p$@4x8Yu$=Qu~WMWYYhM0osck zJB3wc{SxgcvdLTJabKILg>&p}4kUr3BUI(=MXdC9B0!t!%d?d2nA1Bk_=L*&ja2-V zxW8X^)ZH%k-W=LmSj{km0{TTIpOi68i(ykNQoG#C1pxF( z>R(c$5Q^S#4^vs&F>E>mk)6xOosX&oJ4cZO`ov)n3fU$;ix-%GMR-^qcJ!QnT#5$s zB{Ree1=m{IYp!Z&yF)loP|rVpJ*u0+|FQ2RS&Lydf<%TLMQ<+`5|D`0^J;B<_Anr{ z6-C!<+OsS>(0z&QvF(d~s_|{gHl56EooTpk2^S-mae}pD0&8eV)nhl~4~w4(%@!ssxhtj*_dR1o(L|x5LI2(nN)_fBmEx-3}P0 z$wo64)3QK{v$c#IC!<{>eP%!EW5NED5V&0(3JO67l75u3+#?W9>frukiBm5d zwUAauu^RI!fDOsI3MY-`dwM)y%Cq9+YbOGDV_w{8pm|SmL|ozJ4Z$_buv&TLyI}_? zlznLOn|rt;63~dO>t(M2?{&ssqDDvpmy{c*?RVT$*dA^M60lOV3;ptp@F(cd?A)P? zq$^9X0=tS2tMaQd_7_(kxk&rV#n@*Be_A>mM@w6vNn05j+yD;w%xqI6))X58==nJn z6jRg!73Q~+9IQFCrjhLJ)nf3x8*Eo}Dw+4U(UMBAyd4%tw1&%No z2Vh_UrUR|RzX_f({-2jW=U8rkz47W_(>f>Vv zP*%{79g_}bwi%;D+pt7SFvie0f4f{91nkc4?NTNn00=SPF0o*t53y=(=v!tGh#|tH zT9R4Z8NjD>J;0>L?m^G3MgSyeqp^x+?LIlw?XmKz7}vdJEK9|N0=rUq*PMO5? zBR=k}f5>aco5DeyXBE=+KgAYJp!9oCGjB?Ww}8b>hQZE51n17_YFp(Xjh9>V0D_Pj zbyg4)KJs#FUcMBji5L&~ovJ?%6y5V)&(c%RC3v$A65_Xnk)5!bkq2<=Q*s#4U-0_i z;(zAyVlv)kREHp=y_eDoL#v$%1{)%Kusy;C7(Hb1-zBLu+ff9N9UTiO-Rd?>S3>a& z7Ao_sY!Sz)6>VDunXVj`Q>t-|!1hnE2xOmH?c(RromLiDZ|m$O9l3Rhn|Fy#MYek; zB4pG)4Yzf5&O!7Jti{*PD6ADk942c#Pe}^cE|nyE~Rk zi!RgLG~3%~nKBqfCJZlI4WZ&Uu1M;JUV?iqqQB!sx z>*cj9KS9do6DU$&i+I}ylU?xB0^oLYtYZF4TGFYmzU`|axZaE%L>Qvm+XItqJUErH zozQl30LWWI34Kj6*kNU;6M=C8h2aFFeS`?8JNbB%$FJ0Ryl`R0Vw=3zsS>PeAQYZ3ZTbQr~#z4c5xw0W+kTbw4RE6k(pw>il>^vQhTk zv0!{${?WVbS>xslj5X|luPY7-!A3*KgN<$b0=#qAGq)@q0msCWL*&@R-(l^7#+rHx zAu`wcc>5h$gW?L+t4~j!B#Er8?7r9a?++sF0>ZHBv z+_7YuBFT-tjq1N-OuuOBGR8vH04T``!A_HTtug0TMJ%L7PWk^j1CaX>`>*N(CP9MC zrTjeTBdRPFUy4?C>VDo|44CA728d}Doq@nSH;ELoG?-W6qXa754oUz>N0`_DD<^dQ z6g=(S$e9Or1=j>r5r`lPFVM>lRTiMZjI}PvTuHR1`)PbKNiv!*t5?L-O|;>x%yP`&JZg+z%p$W)Bp@5;{X< ztkSETLgA9U>p~{`kVVDhw^16WVvlBPmDzmgPlVb5Jyz#K>ZDoMb(Sja6Q!&`n2V}F z0@4Js2mTs_pcI%n#Mg=_7I&>h7D|7v0AW*JJpvJ4V^Efv6{1|G2?w9U%g6`8GNx;# zF@ko+CHl5~VexFQKXaw}loK8pD`d&W0AS*h$!#COc9*(l27>LtLs+tuT^tg9tlJJWp9T|2ap7{_5Gs$VZ_gyQj+aeRnCph;rAOkqwJ!?P4u#a# z$?r5{jVci%RX8pTGbn`}Rp4-;4aNM~y(2d(U9N|ePhC&#E^8U?Eob-zEZ?{hi{3+p zu`(neHIE3qdN=qG&Z~E*5v9_4z>(Fhsn2@o+6I$MJ!<*fwkj|}1fRcz3r5na1bcii*85~t2Ev?p%&#+;d35|AIhU$VelfkVKyUJ7=yxoWgOwR16?LSEAJtk69{#|RuH6YhNUQa1S zgJ;(G0-2Tjko=83(NfBS@|i-j7#2hYcnGV=3uA*UQJN=qY|`>Fdukm>yJ$?W(szhh z29aBA$SYl)JOuB5|3kTR*ZK3ql~ZIc_=|cz1b%im+D`HjZm@(Atd;Tt_U;PV0U{N7ng8Qeo>ZO(VVp!JRb; zKn$*S@ygaOz?D(GkcFPy6G*!M>2PAm2fvW4-<9$4UNYd+Gm*E;VD8z9b%184#nI4> z=2uXg&={j3ug_%-50H4gz)946{QE)JHkdmV_XmHC%fgR+C)eW~nQGgWr`k@3>Bx?k zjyC#<3)x#AxM$kz9e=U7 zmi%8i76)qLd#sF^7vgRZASc}olk1ABYA<_Z+E(U>*&Wx28_QuDuOpHhKq$a;LZev9 z0+ccocLZI5Q7!?tDoNET*uT|!Ce7z^y$NDjW5lR->*nGO&QR9e`3n_B8wZlzE) ze-&(pkSp;-T_Erl^BO0>5!0tyVe)Rm#YAA`dA1EbK`;}wi&Kza*uwJw>sc#yDi}qB zKN{e)OKAnCqA6ywEkNsMTXR2COqiF~d)V_miZ1x2%frt}oH5etEeM;ue%cfb9k16Pwyo z5>*#{UKXE%fSnDWg*q9vGt=-;P;U}5D}^ebl~J!;z?e(xnGV$Zy}bz$8{XgwvhC~r ztsy86pZ=@72M5141xGFOiIN7*CThZ43f?BOEC&=9fCp+!`7-6bupXf~PqreC)y)Ud z^=X=|;@gShCHGt+LD$KIQ4lF!Yc8GCJ4RRH@eHfunY43Z)7M z<@%TGN+w>x@gj+s;Q&34ItN|=+!@2#>T;8^`^o-?PcZj&Vu3jBF!#Y%UJTI~B3anykjbs03w~8l+i8{8{z~)FAEb{2sI^${ zstw!-^G9d~r)HYqjV3x_p+^qt10K0h*&h% z%b&rm#_IWbnPG@FTh!lh8P59e>glM;t=NzP3R_20KNLY5LpuW&_gXju8UW4_+^c)u7_#HdE#k`C{^tx0graIG zZu8TN*tlY70)VpO_$g`r{iJJ@id}nT5dc~4Y8l)d7H%=h?z&fTN^R)&+jHjYZCoth zcApdSqLdA{zWXD}ACkzC2Qd-j3=W8N9Y#NEgyVR_m?}lU$%<0{~T} z{+?(9t-%Fa#cTufDLwtr@U z;)kv9xa%eYgbdwzPpxWJTWo%v%C+Pfw_#hNF0Jh<&B@;hLs8QE7Ui6YlHpm!#TNC|^3V7RB~xpYd3zs^kiwbE+7LeeMPYe@d-rGZes z3(-_stli%Fv>qtk5@CR~`T!zQ^J(h1{?cgRH z2=ugu09t>x$JchLiy@SVH;|muezL zXHPbF9}EUJ{S`AcJw{K(g0Mm*mLW%#LT>-UjZuJ0szf@r1Mo0qB_ZyWJ)su9Ts%+Z zW*M@h65}qyG`N?IL){N297;fTVq?`D)hcL`7hrTAJehhpAYdL=E$CG#qjAevP!oGM z?k&!PwK?&ud@|>FBUhq61>ol|3a8)qLLb|-*vN_g?kdZSIlfxiveVJd1Ous=8&@II zn73qbx8RAujG|LDcxZ@7sM(iPi{b~t;{GKiP^&sS6V>;IEfx#vx7Pch zAAvV2RzX)~I-@=d!cWe%7tWJyDe9T1;NJz4+*c>|^AckjCn9%dJmpV=&eTn|CICs^ z$7tK37|IVJ1$*LN$97!6GVdkVav?V{g_OqUj|Q2Jc0qI5h)BuabOgylLlZqA@cRWP zlLIxx=8zOSDWIqKk(}B54h;V9Q>@kciUz-g++}&BPOb)jyQ(CMhb48hK?M0%Eg7Y^oYI}x5gai4708|t|G%x#4=H|8WCq8B zK3*+!7^01L&?>D+Z+jzC^~iSlLzC0C%5c#Xu7(97+x;BmdIjH zllh2l!A#e6qC>u0Yy*R%%`#paRRCMYZP8qNwH$q6zY>ng?vml?$^Cvk_}r#Rt57-# z6x(+k?hD`#u>%Yexzg5;aYYlMf?s~w;U1Am6&eO6i9%kjZfw5i6WK*V-#!*YcpN>i zzZU(1l@0;1&dmje$mXpa)xSH%joG4ts9d|(Yit-0!LeE)0NsOYZGCw42tTl{`GH$) zy$JmNcLw>l(&aL2`J>{RGCt^SLtlEDh#2JIN$xTS3*D|BO9L!*E@v2!Ox~)jLldR@ zgak4{LSO9XrdQOhD;$5#AvCVwuR22o71Q7Dn76+%x&!MZKxZAw0XZ#HBo6(TIorJ9 zO%yHa-w!)VI1zOO$!dd2bXt&M;{)_LWho6!_D|dB{e$O%1L-nd-U=^+130HQmDUXS zxUL&?Ij_1M{>HX=*BMPy4!2=D++INK^rHjKW;EH#Sa4e>7z!^Mrz$1RU=7afj9J3< z^)tVY1*R=EfcvwtihQfRmVYGl&@HqGQJ!PNZuwsIbq+hlJyz1(MiyHWBiNEfgN%pj_OsK8fDJ~oJoF6&Gzh^@%kQcIP}cv$p^Igi6TCvIN2%;N zT^;}2m-9rj*dY`c2ZLpC0MVFK~}7XiyvB8vJ``QzX+o!Ir;Z7Mq$lkFPXS z`l`f;bZ%7w9TE5u`j1X{2K!$9aJYmW(b$bq8q|7C$+_2{te5w*v`tzq92D|~-(<>S zzuo`PnGqxHdx>fuPZ$T-JF>ktrx+5A3Kl5wM*l5ja1SIZr-m!u)^Zie`#HYUsp)`f z{S54m^Gjgab~$1^>4&f>hYAYOGYwL6Hb3X6!97m}di6ykXVjjhBAQd_n*EOdOat_> z^fd|_x^tIrcbSbO5E{66X{pT`t_C_^eE0z6xA5Vv#K+pMCX4i00jti3(2 zKC8{~T5$IOSa&b-lO9GvfwaFX51_}p?C%j~9S8nz$KLC}f&7v$sR9Bw$9@V@VzE1L z*ocl7$d$O>l?Vn(Q}uYcVY^V&l-j4VE#u_oxEdOd*@l**hzymjc%sqvb6wD_lj)g% zru@V~3!f1aHmMO*CpdT2V_;yftLrmOi`g9nqOfSI)Cslj_>2BU#uoIU*|vv@=`^E! zVSV`wdpoKlQbi!>BaXdpqY7uB9XG?^qXPEZF%9f=xv8xJ*8b|Tp=1ylg?M!@t!`Bu zVp$nKo{7$*li4)mvu!Q%3V770$8#YTsTmovz4v8W9DaI$l!wh@`vmog)P2b%Jh zWa&85cZM$prn%ntWf|~x^aW3odd?X~BN){LO~?|gvH6th|6OH}vfD|#=NlMFv^Thw zmfZ(9?8d+~W#)-Q;|o*IGEl+impY`*MmGD&uP~t`rW(s2sF?6v5kkA_5fT=WmqP!R zmL@p|J4BN#{t{A4PkXEnf%uX;E#Z(2HK-bSeJSw6G5kt{LBUO0xZCQoTvTMUHVRIDr%=^+fFPftE)d2|Q;XB`0?nr`9>|XcqCWnF zR0bnI)K_F{%E$d0yuWCl8PB#y=mopC;*sl0S{O@X|Hzbr_?;s1-wO0%c!A7jCK5`; z=F+wJj++!L7p~tdreEvXt&*)v1;5g70Y|!2i$4S``*6fOb{SA;F?7bZGI8pzeMKv98l2L9uXx`;;zzywVQN>sX>apee zk60@>dn7XrVn0+=P=BKcfX&oEILJHvI|GRY?4$O0X}2M!I?~8 z7_1pVUJd2?#!5+_hXN}sjA~NKu{XPQ#i9agdF^fH?+qrkt+!_Z_eTfR?|}_z>K=AN zF!L4_+i>0G0D2pC)?hn5flt%Rn@hQGwi3v_y%GboF^CrFmy%`!JMiI4B5m_T`CLgp z5>*c0TBQz~6%TL;sQNC>``Rc8nYPZTR1|{3)(?q!SoWQydaz?-k7+z1vx8BU8HshE znwhxk-JfW1tnYZqSe$IZ5>K2XmkA?0!i@Ldn2AbDrFS#W@pcIo8qu=BbpbaGt?khk3l{shEh+Rv85w+4$X9ya~G!}&z?bvAo+y`mb zYl~PN)yrG}4-M*}1qF!bCr%l*OEvbkN|o{AVpbP~T2>LugyThBSPxZM;>AN3Wq;vk znoBTQJk(q^k_%aWWwN^v+42tcOjkbc066=`lh1O9aH{l}|XR z!d#PDAT>oT5vs?!r*kzB6ggG?>XIl+Z-a?ZcV)byQPo%~%l$}#s0bpqC>Ze1QuxVm z-9gj*L!7T0aI~X8#tj-uhZZgo-BYQ4HB)lGI}fXx*uqcd>dOI=|M@% z4jZXxakk!8&|E#f7M&RDiM>?;{Necdr%zHDw+~cI^sdJES}hwuLN}y+_y{*;XL!Uh z&DzLZyre$onzDv{S>OlyUK`->lVN8mPM5DG;}N+@vz1-PXPTJRCx6+VJ zDVZPkK)S|_>Ufl$7M!5bYh{F4!}mjgB1;NgOOtW<^$nfc|dbSQ?a zfD|)3M`l%8D2(9tQtU* zIjr=d1~rupi8t4nL=U5v3@>mNq|IpnBaT^_CLX2){Dr*1?W#Fhb_X_=AMb8@9p5*_ zd;a3@o>rUBI=WcLFwNsDLkaQ_miyCEpf4p#@nE07vFu*F`U^`DvJ+Sz0RAm#03Ue0 z4F)-#Q}h0XXqg9|uuP;Kye!ag|% z;GHB;DUb?z0e|tKmpxV`A3@q`j1@rM+>^KRNr9go<-*%yYx;oS@TO8bZVMi z7qXtcQl;^&^qY0e%RdE)BsyaCbipA_JuS{MG4z;oy_ zu-@oMN0@CWfcRA7$(jb(0QL~?TEDp+cy0Gelwho{=JXzQdBW;+%j?j#0YWb%EQQQWI z4>yHm$uPlvDsvytpi;D};`hYAy^AR+Bm)J1*Bp2DSZMKqlN-%7N_1T&wI?I;O3(k^ z$fcYRB1r+!E+omlVM7JH8>HT~-Ug#ZHmo|UK?M=`)q4;_+b$%aaRWj>6S1b1a{QOQssRW0O z7j_W^C;-3=QH>1*iWyMv}>z(1N&; z?i1E`vOgU3eae74xHtP?v8sbdXtdXk;3Nrr?Td6fC`vK6Bl8}CWNV?&0{Ufy9RpF{Rl{ijEYeJcv4>;8dVP@kvKOOWcgsp=#*FRUXb=^%)@F3JfJ$_Aar*cE4N7)GAL z9Vklof*q<#?{e-^j}r^q&XX*Ox9<-rPhVJ)N4;Y}&^@xZ$_$Ip-kafCZpPuSpC?riWR@`XSMB|tn|Zo>@l>8iv$%4 z2D(jH>71f*Xi%9qOgO$tm{g&Z8Vsax{O6fzJ_ej)>ed8oEUU*|AJ!o|&{(;E6uo~Z zbbqw*CSJ)<>#9Pja~Z!j@N#P+I?ljSqYu%i5`?w%oqD&Je@MsACGbj6T9KLeoAiF` z{DwM!T%?$3=2+=hR|F)!r~dpc>-YnlMc`V(lP`-*7u+ z+^FSG7f)0NNS?~mg$TO{qO7!fl|Ph3n_u;EcFU6Wiina9;+aF(+^N1Iml4Ndrq&t_ z1@|&cPNa4O`xwi(@Y?wPS;>21@NpsbKKss0_u1MtfUZ23-0%(U=HB3y!Wy|r$4>{-lwQo!o+Wqbj$c58t(u&vN+KBm<3j9$ z(+15;TXZ^3=U8MIo!@eEXBBVvo_|*F!>qyxfqXjDoG|96OJ(uw1E|g5Ya_p-cL6%G zQEo^KraVvib`W#+JB@fno@v$hHtgn*!jVI_fNP*#Zn{8*LPfSNHp?(|A`kglf;X*y z%Nd1(_H{cPc!y`t&hRpoW|cvjl~xIviZ2H(#djE5ynNqdgqEtUP|?*Gz6upB>IK07BAb}nXu%_>fAnhw4(^@v zvIpAgA6x}O@;`|vA&?h0Y&F-T%jqJ)x)-P-fETYvpNm_XQl0TwYPtTDX3z743E(*` zpuTce-7^(c=pb(lNhw1N?0Ap!F?wBm&?{F_m@0vpNeTt;eXHwPY?;@mn4b|5Y;B}J|haLijC9I;ODPoU{Cy)B%mew zAw=Il67E1u+aq%ktWD2baT9ybmcBy`^%yBJ_!uYGMl*SWLNtLY^ktmbbvJ;tPAB)` z;<$Av;K3x(odJui5*FuUB8*3OirE$tOJo-JYmN+-f9it7JMjGAOc$0^;JbK16Q(U& zbD7ZPIsJ)JO$8dBkO^msWX>Mzch|%B4{ZRWXGv)BzfeRDD%|)2f$gK2RV?lrLNrmiDmN2G1>JWo4m+%g4WB~^p>0s>%*fz@`v=4*VhX-^DdTWB*JLjU^I3TwAM&u!GBmqr4$lxbkqCi1?Ca21OEDTgyA%+5WNr2xoiVs~k;$qt{h5;Qv~Tnwz1#4I1tA_Hl-PO)D=McB})#GBTcxRV8< zu64TjzCs~}E;213+(wlt1l0lfqz91tgk2tAU{1YwV|Nq3t-n*%t#*UaQaD!kg{|n| z1>$l5Ia9{F1@%H>ps+m^L<^(VJo02Jm-6p^Bk}GrEh+){;P+1bzg~u;SW1R`he6_s zaFa4Q2UEU*rvap~m3$tkN) zwh=WWjUzTfH2#{NgpbV`+_|Rgd0&p^u7~Rc$$8|FXM2?l~)HA3*2LZN6 z;>BOKZBlr1ntQ%vdw@bj2L90~+j>2Ue}U7JS?K1R=(t_47>a=VBntkJS#J1vahNiS-ka!I&2C)5HxAB$N>s3*;6wi4q6@#=eKDY%W2 zj(U}j@cr4qt}zrwA_M{3R42z8KL&F$`3VCP9gb|XeO=~as0M-!v2~+2cog)LennB4 zim=0@Px# zN+M+5&wQN^YQ6S0Lwn57`bkm4aE>}owG4a==vGYo_1^&xLYX^>x3|6Qh#1yNv+zT` zJzyMAI94h{;Qs$e$@p0^oZg`Pbqsbb4$&@s2m!GmS(NFEhQm1&MHE$x>FmT-hr-O7 zI+#9=DNi0DaI&%u-~X7SbHK_YQ3gD$-Y&f=GTj0#ggPv{|Zv6K`&{W&0p zs0OR1zY2L@I63S=+WDFzXj^pUtM~WeXEJVW9i$%x#WeKi$t78T3onL8w8S&J@>hKb zNfD&nuKguH-ln>V6RJufw8_ITuj~@>R<`$4XBT06j*Ox^>y2E;wM%vAD@+zRIuWj2 zAzrp0v;sq)_4zFh2Dus)tikZ6gm>QjHr6+kq-I=h(D9w@O+CW9Xny5X;Ru?+=k)^M z-H_w)g;>YD$iRc)$rWHy#QO_{-nek{4(^m34t!sRVJ|HL5DTUWJr>W;eKXfM6LV5k zw*+~r0^+8({5RT3zKLoD-xVTv3&DJ76W7W&R#u7WZFCKJ773sgtw0!V&CGKH(ue=f zv}w`A95xLvqrO$@4PlrSVr7RN5DLp#TRMr7P(eNd#}s^Ge4oo1J6~;D%e)Dz@CFcq zlB1%tHeY-7Gtx49SdVn#Fv)iWr|ESaU_yyY5Cw*o0%E;afmnfincK_w!vIa8=P{e* zL~T6Y^=1-nZLT%A(I{Y5rIx9*$Z|^d(+A8cYK7Q2y=xcx=5;^lPeS*&CdO4o(`g&9 zdg$LkqHMsJFn$gSqVnY#qrU1e4-dsFaPix1-g_o_RjVkWhRI=LszUut$j>0LhpJe;eb)W zUk_UfF{K%p#tk7#7%nX8u4MF_-+4u-`M>M!dPmfFxCBn4H+y&g${QM#7AIqV!{Bc? znd5deRszmD+=L>72wMzmV4C@vjE-x8MAx&-sn&CzbhZ|t*+%6C1ApHOdlg50zy-{K zauHXy!HH*j#Lr;!2G_nu@2OgOVPpo$Sz{<_NB;}N?Fn3c{RSjf?;|nLG6|Flnhi8Q z9IOZ6+LV>2Gqty(S-qEs;@<5OgVm!wx0>eb)3C5}C}L|16tA(cu@HCBxe}IU47co0 z=Z#?;wO29^aEI8%kHqYPD2><}d`JQBq5Px^1rr3}sqKZ%k>D$!;n(zU zaYwk&0IP;ylKyUe?gKT$!>N&B^v3skN<^X;^|?LCmxGOAQRKI z?rPFQ@Hw)M@Ou5!IM3R4>lPk~FLGhZS?=3k8ZZRhRsz+lXLKbM2;)*+P1Lq0#5d^k zWF`z_B93G}JGsQYK>{@nVb91Io}VbK*!x38VtQ}5FhZ3yK$d?)tGc6?3q)f}sIFEfM+rr;`I{O}JObDwqe1I161^CL(r9`{01Vw-# z1JKzE8WdH~sdUZSQ84P-UhshImBEj%=v&Ks;PZK7hfbq~ui$=)H-VTDo-2cvzPUewFfRa&{us(`YQT|R5OsYOV6iw2aH zT0ldq=R1ndeo!T!n+S@7T(Eu&^NEd>?XVk1+{bR*yy<@Ghmk*47vw=SAOSV!EiR@8 zQYV!9tr*@O)_C5?!KdB7*1&gcz}tV}mE5mm)FM5P4&#Uw4Wge*2?SFG8&UEyopEDE z3a(gg?5(*CI(SpFD>*SL4p-#K)I$2rxZ^W4bY7?~Uinp*x(1j-!-0T5{_ZN6^S`={ zWW>@}3ZVTg2`zGL@3S7LJfaXC zbj@wQeaA+knY_IUx+%PJV;@sC0Ayxki#tUK5)LDR9L{}%p48baPc58K{)x)5a;rnM z>w@reAfm5C1zRM85G9h@OI5~L_j8BQX$4pG5?y-?Q}X@VndWQ|M5Ur2m7pEF)r(c#!;6f1qthIKM8TGWO`iOZ0j5=^-L~()%RYE_^6&$Q%MfV zf;i7sZfZ)N@T3VU&Z6M{W&l5BPx0U1Ax%iO$7T49f=QXt8Fo9tC;ao!CNwe5^UknS zyyJwr6jTB9F84C^T^7f4*R$-xdZ_v=7MC0ZgaMLNGfN(E(K$3dPhW+T!uM^4{jkKG zPwr-u{(ZQ&Xa+rKBFJQ{+?EPFXNpQWryi>Z<}s-n+LY^lk>tRFEPctF0NOk<1?_fM z0odRe_ZenaGL1*YoulJ@FGwHv!H7}t0fHVL`pu2B$Vs9x zkOR<$*gvvp>&iRp+kNOmr-rZOtiP~w7X)c#jBbw0wXqGAzN{m2ooAZH9U+yc_6l+q z2Kv%}RznKoc0Ti5YH(X|XNjsNW;ndseJiauqupQ*6^HS9SfVG4!&@VoHx-Y87T_+0 zXE)!>+R&%*0QeKbZ<;kUo_?k;tg?bkl6So{=2#v0J?itpCg&3IUJEbS>x$^&-%^3( zdcR{d${xdb1`$Kfd>nvE*~AINEvq!gjh%S}sC7)TfqIk2>J{RV1*iVM*v%WK%#Cyb zy6- { + let mut e = $projective::identity(); + + let mut v = vec![]; + { + let mut expected = $expected; + for _ in 0..1000 { + let e_affine = $affine::from(e); + let encoded = e_affine.$serialize(); + v.extend_from_slice(&encoded[..]); + + let mut decoded = encoded; + let len_of_encoding = decoded.len(); + (&mut decoded[..]).copy_from_slice(&expected[0..len_of_encoding]); + expected = &expected[len_of_encoding..]; + let decoded = $affine::$deserialize(&decoded).unwrap(); + assert_eq!(e_affine, decoded); + + e = &e + &$projective::generator(); + } + } + + assert_eq!(&v[..], $expected); + }; +} + +#[test] +fn g1_uncompressed_valid_test_vectors() { + let bytes: &'static [u8] = include_bytes!("g1_uncompressed_valid_test_vectors.dat"); + test_vectors!( + G1Projective, + G1Affine, + to_uncompressed, + from_uncompressed, + bytes + ); +} + +#[test] +fn g1_compressed_valid_test_vectors() { + let bytes: &'static [u8] = include_bytes!("g1_compressed_valid_test_vectors.dat"); + test_vectors!( + G1Projective, + G1Affine, + to_compressed, + from_compressed, + bytes + ); +} + +#[test] +fn g2_uncompressed_valid_test_vectors() { + let bytes: &'static [u8] = include_bytes!("g2_uncompressed_valid_test_vectors.dat"); + test_vectors!( + G2Projective, + G2Affine, + to_uncompressed, + from_uncompressed, + bytes + ); +} + +#[test] +fn g2_compressed_valid_test_vectors() { + let bytes: &'static [u8] = include_bytes!("g2_compressed_valid_test_vectors.dat"); + test_vectors!( + G2Projective, + G2Affine, + to_compressed, + from_compressed, + bytes + ); +} + +#[test] +#[cfg(all(feature = "alloc", feature = "pairing"))] +fn test_pairing_result_against_relic() { + /* + Sent to me from Diego Aranha (author of RELIC library): + 1250EBD871FC0A92 A7B2D83168D0D727 272D441BEFA15C50 3DD8E90CE98DB3E7 B6D194F60839C508 A84305AACA1789B6 + 089A1C5B46E5110B 86750EC6A5323488 68A84045483C92B7 AF5AF689452EAFAB F1A8943E50439F1D 59882A98EAA0170F + 1368BB445C7C2D20 9703F239689CE34C 0378A68E72A6B3B2 16DA0E22A5031B54 DDFF57309396B38C 881C4C849EC23E87 + 193502B86EDB8857 C273FA075A505129 37E0794E1E65A761 7C90D8BD66065B1F FFE51D7A579973B1 315021EC3C19934F + 01B2F522473D1713 91125BA84DC4007C FBF2F8DA752F7C74 185203FCCA589AC7 19C34DFFBBAAD843 1DAD1C1FB597AAA5 + 018107154F25A764 BD3C79937A45B845 46DA634B8F6BE14A 8061E55CCEBA478B 23F7DACAA35C8CA7 8BEAE9624045B4B6 + 19F26337D205FB46 9CD6BD15C3D5A04D C88784FBB3D0B2DB DEA54D43B2B73F2C BB12D58386A8703E 0F948226E47EE89D + 06FBA23EB7C5AF0D 9F80940CA771B6FF D5857BAAF222EB95 A7D2809D61BFE02E 1BFD1B68FF02F0B8 102AE1C2D5D5AB1A + 11B8B424CD48BF38 FCEF68083B0B0EC5 C81A93B330EE1A67 7D0D15FF7B984E89 78EF48881E32FAC9 1B93B47333E2BA57 + 03350F55A7AEFCD3 C31B4FCB6CE5771C C6A0E9786AB59733 20C806AD36082910 7BA810C5A09FFDD9 BE2291A0C25A99A2 + 04C581234D086A99 02249B64728FFD21 A189E87935A95405 1C7CDBA7B3872629 A4FAFC05066245CB 9108F0242D0FE3EF + 0F41E58663BF08CF 068672CBD01A7EC7 3BACA4D72CA93544 DEFF686BFD6DF543 D48EAA24AFE47E1E FDE449383B676631 + */ + + let a = G1Affine::generator(); + let b = G2Affine::generator(); + + use super::fp::Fp; + use super::fp12::Fp12; + use super::fp2::Fp2; + use super::fp6::Fp6; + + let res = pairing(&a, &b); + + let prep = G2Prepared::from(b); + + assert_eq!( + res, + multi_miller_loop(&[(&a, &prep)]).final_exponentiation() + ); + + assert_eq!( + res.0, + Fp12 { + c0: Fp6 { + c0: Fp2 { + c0: Fp::from_raw_unchecked([ + 0x1972_e433_a01f_85c5, + 0x97d3_2b76_fd77_2538, + 0xc8ce_546f_c96b_cdf9, + 0xcef6_3e73_66d4_0614, + 0xa611_3427_8184_3780, + 0x13f3_448a_3fc6_d825, + ]), + c1: Fp::from_raw_unchecked([ + 0xd263_31b0_2e9d_6995, + 0x9d68_a482_f779_7e7d, + 0x9c9b_2924_8d39_ea92, + 0xf480_1ca2_e131_07aa, + 0xa16c_0732_bdbc_b066, + 0x083c_a4af_ba36_0478, + ]) + }, + c1: Fp2 { + c0: Fp::from_raw_unchecked([ + 0x59e2_61db_0916_b641, + 0x2716_b6f4_b23e_960d, + 0xc8e5_5b10_a0bd_9c45, + 0x0bdb_0bd9_9c4d_eda8, + 0x8cf8_9ebf_57fd_aac5, + 0x12d6_b792_9e77_7a5e, + ]), + c1: Fp::from_raw_unchecked([ + 0x5fc8_5188_b0e1_5f35, + 0x34a0_6e3a_8f09_6365, + 0xdb31_26a6_e02a_d62c, + 0xfc6f_5aa9_7d9a_990b, + 0xa12f_55f5_eb89_c210, + 0x1723_703a_926f_8889, + ]) + }, + c2: Fp2 { + c0: Fp::from_raw_unchecked([ + 0x9358_8f29_7182_8778, + 0x43f6_5b86_11ab_7585, + 0x3183_aaf5_ec27_9fdf, + 0xfa73_d7e1_8ac9_9df6, + 0x64e1_76a6_a64c_99b0, + 0x179f_a78c_5838_8f1f, + ]), + c1: Fp::from_raw_unchecked([ + 0x672a_0a11_ca2a_ef12, + 0x0d11_b9b5_2aa3_f16b, + 0xa444_12d0_699d_056e, + 0xc01d_0177_221a_5ba5, + 0x66e0_cede_6c73_5529, + 0x05f5_a71e_9fdd_c339, + ]) + } + }, + c1: Fp6 { + c0: Fp2 { + c0: Fp::from_raw_unchecked([ + 0xd30a_88a1_b062_c679, + 0x5ac5_6a5d_35fc_8304, + 0xd0c8_34a6_a81f_290d, + 0xcd54_30c2_da37_07c7, + 0xf0c2_7ff7_8050_0af0, + 0x0924_5da6_e2d7_2eae, + ]), + c1: Fp::from_raw_unchecked([ + 0x9f2e_0676_791b_5156, + 0xe2d1_c823_4918_fe13, + 0x4c9e_459f_3c56_1bf4, + 0xa3e8_5e53_b9d3_e3c1, + 0x820a_121e_21a7_0020, + 0x15af_6183_41c5_9acc, + ]) + }, + c1: Fp2 { + c0: Fp::from_raw_unchecked([ + 0x7c95_658c_2499_3ab1, + 0x73eb_3872_1ca8_86b9, + 0x5256_d749_4774_34bc, + 0x8ba4_1902_ea50_4a8b, + 0x04a3_d3f8_0c86_ce6d, + 0x18a6_4a87_fb68_6eaa, + ]), + c1: Fp::from_raw_unchecked([ + 0xbb83_e71b_b920_cf26, + 0x2a52_77ac_92a7_3945, + 0xfc0e_e59f_94f0_46a0, + 0x7158_cdf3_7860_58f7, + 0x7cc1_061b_82f9_45f6, + 0x03f8_47aa_9fdb_e567, + ]) + }, + c2: Fp2 { + c0: Fp::from_raw_unchecked([ + 0x8078_dba5_6134_e657, + 0x1cd7_ec9a_4399_8a6e, + 0xb1aa_599a_1a99_3766, + 0xc9a0_f62f_0842_ee44, + 0x8e15_9be3_b605_dffa, + 0x0c86_ba0d_4af1_3fc2, + ]), + c1: Fp::from_raw_unchecked([ + 0xe80f_f2a0_6a52_ffb1, + 0x7694_ca48_721a_906c, + 0x7583_183e_03b0_8514, + 0xf567_afdd_40ce_e4e2, + 0x9a6d_96d2_e526_a5fc, + 0x197e_9f49_861f_2242, + ]) + } + } + } + ); +} diff --git a/constantine/bls12_381/src/util.rs b/constantine/bls12_381/src/util.rs new file mode 100644 index 000000000..bd25dd06a --- /dev/null +++ b/constantine/bls12_381/src/util.rs @@ -0,0 +1,174 @@ +/// Compute a + b + carry, returning the result and the new carry over. +#[inline(always)] +pub const fn adc(a: u64, b: u64, carry: u64) -> (u64, u64) { + let ret = (a as u128) + (b as u128) + (carry as u128); + (ret as u64, (ret >> 64) as u64) +} + +/// Compute a - (b + borrow), returning the result and the new borrow. +#[inline(always)] +pub const fn sbb(a: u64, b: u64, borrow: u64) -> (u64, u64) { + let ret = (a as u128).wrapping_sub((b as u128) + ((borrow >> 63) as u128)); + (ret as u64, (ret >> 64) as u64) +} + +/// Compute a + (b * c) + carry, returning the result and the new carry over. +#[inline(always)] +pub const fn mac(a: u64, b: u64, c: u64, carry: u64) -> (u64, u64) { + let ret = (a as u128) + ((b as u128) * (c as u128)) + (carry as u128); + (ret as u64, (ret >> 64) as u64) +} + +macro_rules! impl_add_binop_specify_output { + ($lhs:ident, $rhs:ident, $output:ident) => { + impl<'b> Add<&'b $rhs> for $lhs { + type Output = $output; + + #[inline] + fn add(self, rhs: &'b $rhs) -> $output { + &self + rhs + } + } + + impl<'a> Add<$rhs> for &'a $lhs { + type Output = $output; + + #[inline] + fn add(self, rhs: $rhs) -> $output { + self + &rhs + } + } + + impl Add<$rhs> for $lhs { + type Output = $output; + + #[inline] + fn add(self, rhs: $rhs) -> $output { + &self + &rhs + } + } + }; +} + +macro_rules! impl_sub_binop_specify_output { + ($lhs:ident, $rhs:ident, $output:ident) => { + impl<'b> Sub<&'b $rhs> for $lhs { + type Output = $output; + + #[inline] + fn sub(self, rhs: &'b $rhs) -> $output { + &self - rhs + } + } + + impl<'a> Sub<$rhs> for &'a $lhs { + type Output = $output; + + #[inline] + fn sub(self, rhs: $rhs) -> $output { + self - &rhs + } + } + + impl Sub<$rhs> for $lhs { + type Output = $output; + + #[inline] + fn sub(self, rhs: $rhs) -> $output { + &self - &rhs + } + } + }; +} + +macro_rules! impl_binops_additive_specify_output { + ($lhs:ident, $rhs:ident, $output:ident) => { + impl_add_binop_specify_output!($lhs, $rhs, $output); + impl_sub_binop_specify_output!($lhs, $rhs, $output); + }; +} + +macro_rules! impl_binops_multiplicative_mixed { + ($lhs:ident, $rhs:ident, $output:ident) => { + impl<'b> Mul<&'b $rhs> for $lhs { + type Output = $output; + + #[inline] + fn mul(self, rhs: &'b $rhs) -> $output { + &self * rhs + } + } + + impl<'a> Mul<$rhs> for &'a $lhs { + type Output = $output; + + #[inline] + fn mul(self, rhs: $rhs) -> $output { + self * &rhs + } + } + + impl Mul<$rhs> for $lhs { + type Output = $output; + + #[inline] + fn mul(self, rhs: $rhs) -> $output { + &self * &rhs + } + } + }; +} + +macro_rules! impl_binops_additive { + ($lhs:ident, $rhs:ident) => { + impl_binops_additive_specify_output!($lhs, $rhs, $lhs); + + impl SubAssign<$rhs> for $lhs { + #[inline] + fn sub_assign(&mut self, rhs: $rhs) { + *self = &*self - &rhs; + } + } + + impl AddAssign<$rhs> for $lhs { + #[inline] + fn add_assign(&mut self, rhs: $rhs) { + *self = &*self + &rhs; + } + } + + impl<'b> SubAssign<&'b $rhs> for $lhs { + #[inline] + fn sub_assign(&mut self, rhs: &'b $rhs) { + *self = &*self - rhs; + } + } + + impl<'b> AddAssign<&'b $rhs> for $lhs { + #[inline] + fn add_assign(&mut self, rhs: &'b $rhs) { + *self = &*self + rhs; + } + } + }; +} + +macro_rules! impl_binops_multiplicative { + ($lhs:ident, $rhs:ident) => { + impl_binops_multiplicative_mixed!($lhs, $rhs, $lhs); + + impl MulAssign<$rhs> for $lhs { + #[inline] + fn mul_assign(&mut self, rhs: $rhs) { + *self = &*self * &rhs; + } + } + + impl<'b> MulAssign<&'b $rhs> for $lhs { + #[inline] + fn mul_assign(&mut self, rhs: &'b $rhs) { + *self = &*self * rhs; + } + } + }; +} diff --git a/constantine/csharp.patch b/constantine/csharp.patch new file mode 100644 index 000000000..2e8678bcc --- /dev/null +++ b/constantine/csharp.patch @@ -0,0 +1,25 @@ +From 86aa67b0e3775514cc484ddd2adf6b5dc6e26803 Mon Sep 17 00:00:00 2001 +From: sirse +Date: Thu, 26 Oct 2023 13:40:30 +0300 +Subject: [PATCH] Patch csharp binding + +--- + bindings/csharp/Makefile | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/bindings/csharp/Makefile b/bindings/csharp/Makefile +index 5158aad..af3b2a8 100644 +--- a/bindings/csharp/Makefile ++++ b/bindings/csharp/Makefile +@@ -39,7 +39,7 @@ else + endif + + INCLUDE_DIRS = ../../src ../../blst/bindings +-TARGETS = ckzg.c ../../src/c_kzg_4844.c ../../blst/$(BLST_OBJ) ++TARGETS = ckzg.c ../../../../target/release/rust_kzg_zkcrypto.a + + CFLAGS += -O2 -Wall -Wextra -shared + CFLAGS += ${addprefix -I,${INCLUDE_DIRS}} +-- +2.34.1 + diff --git a/constantine/go.patch b/constantine/go.patch new file mode 100644 index 000000000..9ef34988f --- /dev/null +++ b/constantine/go.patch @@ -0,0 +1,43 @@ +From 90e9a518ca03716ef1d9d77e263db25b56030867 Mon Sep 17 00:00:00 2001 +From: sirse +Date: Thu, 26 Oct 2023 14:09:11 +0300 +Subject: [PATCH] Patch go binding + +--- + bindings/go/main.go | 13 +++++++++---- + 1 file changed, 9 insertions(+), 4 deletions(-) + +diff --git a/bindings/go/main.go b/bindings/go/main.go +index bdd5385..155fc81 100644 +--- a/bindings/go/main.go ++++ b/bindings/go/main.go +@@ -2,7 +2,15 @@ package ckzg4844 + + // #cgo CFLAGS: -I${SRCDIR}/../../src + // #cgo CFLAGS: -I${SRCDIR}/blst_headers +-// #include "c_kzg_4844.c" ++// #ifndef BYTES_PER_G1 ++// #define BYTES_PER_G1 48 ++// #endif ++// #ifndef BYTES_PER_G2 ++// #define BYTES_PER_G2 96 ++// #endif ++// #include ++// #include "c_kzg_4844.h" ++// #cgo LDFLAGS: -L${SRCDIR}/../../../../target/release -l:rust_kzg_zkcrypto.a -lm + import "C" + + import ( +@@ -11,9 +19,6 @@ import ( + "fmt" + "strings" + "unsafe" +- +- // So its functions are available during compilation. +- _ "github.com/supranational/blst/bindings/go" + ) + + const ( +-- +2.34.1 + diff --git a/constantine/java.patch b/constantine/java.patch new file mode 100644 index 000000000..a5a7fa44d --- /dev/null +++ b/constantine/java.patch @@ -0,0 +1,24 @@ +From b1f8f612f8c1bda0b4ea58e01e9a60235a88cc83 Mon Sep 17 00:00:00 2001 +From: povilassl +Date: Sun, 24 Sep 2023 18:01:51 +0300 +Subject: [PATCH] java patch + +--- + bindings/java/Makefile | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/bindings/java/Makefile b/bindings/java/Makefile +index 9be2fd6..1e59378 100644 +--- a/bindings/java/Makefile ++++ b/bindings/java/Makefile +@@ -1,6 +1,6 @@ + INCLUDE_DIRS = ../../src ../../blst/bindings + +-TARGETS=c_kzg_4844_jni.c ../../src/c_kzg_4844.c ../../lib/libblst.a ++TARGETS=c_kzg_4844_jni.c ../../../../target/release/rust_kzg_zkcrypto.a + + CC_FLAGS= + OPTIMIZATION_LEVEL=-O2 +-- +2.37.0.windows.1 + diff --git a/constantine/nodejs.patch b/constantine/nodejs.patch new file mode 100644 index 000000000..710482442 --- /dev/null +++ b/constantine/nodejs.patch @@ -0,0 +1,74 @@ +From 954c55533e265f32eabe0dc863b1add2478bb570 Mon Sep 17 00:00:00 2001 +From: sirse +Date: Thu, 26 Oct 2023 14:02:51 +0300 +Subject: [PATCH] Patch nodejs binding + +--- + bindings/node.js/Makefile | 1 - + bindings/node.js/binding.gyp | 31 +++---------------------------- + 2 files changed, 3 insertions(+), 29 deletions(-) + +diff --git a/bindings/node.js/Makefile b/bindings/node.js/Makefile +index 17850ec..efc9961 100644 +--- a/bindings/node.js/Makefile ++++ b/bindings/node.js/Makefile +@@ -31,7 +31,6 @@ build: install clean + @# Prepare the dependencies directory + @mkdir -p deps/c-kzg + @cp -r ../../blst deps +- @cp ../../src/c_kzg_4844.c deps/c-kzg + @cp ../../src/c_kzg_4844.h deps/c-kzg + @# Build the bindings + @$(YARN) node-gyp --loglevel=warn configure +diff --git a/bindings/node.js/binding.gyp b/bindings/node.js/binding.gyp +index 5ac368e..6cde37f 100644 +--- a/bindings/node.js/binding.gyp ++++ b/bindings/node.js/binding.gyp +@@ -3,9 +3,7 @@ + { + "target_name": "kzg", + "sources": [ +- "src/kzg.cxx", +- "deps/blst/src/server.c", +- "deps/c-kzg/c_kzg_4844.c" ++ "src/kzg.cxx" + ], + "include_dirs": [ + "<(module_root_dir)/deps/blst/bindings", +@@ -16,31 +14,8 @@ + "__BLST_PORTABLE__", + "NAPI_DISABLE_CPP_EXCEPTIONS" + ], +- "conditions": [ +- ["OS!='win'", { +- "sources": ["deps/blst/build/assembly.S"], +- "cflags_cc": [ +- "-std=c++17", +- "-fPIC" +- ] +- }], +- ["OS=='win'", { +- "sources": ["deps/blst/build/win64/*-x86_64.asm"], +- "defines": [ +- "_CRT_SECURE_NO_WARNINGS", +- ], +- "msbuild_settings": { +- "ClCompile": { +- "AdditionalOptions": ["/std:c++17"] +- } +- } +- }], +- ["OS=='mac'", { +- "xcode_settings": { +- "CLANG_CXX_LIBRARY": "libc++", +- "MACOSX_DEPLOYMENT_TARGET": "13.0" +- } +- }] ++ "libraries": [ ++ "<(module_root_dir)/../../../../target/release/rust_kzg_zkcrypto.a" + ] + } + ] +-- +2.34.1 + diff --git a/constantine/python.patch b/constantine/python.patch new file mode 100644 index 000000000..01dae992d --- /dev/null +++ b/constantine/python.patch @@ -0,0 +1,48 @@ +From a8ff3fe1e4372380f15769bbb8490b0089aa2928 Mon Sep 17 00:00:00 2001 +From: sirse +Date: Thu, 26 Oct 2023 13:51:37 +0300 +Subject: [PATCH] Patch python binding + +--- + bindings/python/Makefile | 5 +---- + bindings/python/setup.py | 6 +++--- + 2 files changed, 4 insertions(+), 7 deletions(-) + +diff --git a/bindings/python/Makefile b/bindings/python/Makefile +index c6bd222..99d6501 100644 +--- a/bindings/python/Makefile ++++ b/bindings/python/Makefile +@@ -1,11 +1,8 @@ + .PHONY: all + all: install test + +-../../src/c_kzg_4844.o: +- make -C../../src c_kzg_4844.o +- + .PHONY: install +-install: setup.py ckzg.c ../../src/c_kzg_4844.o ++install: setup.py ckzg.c + python3 setup.py install + + .PHONY: test +diff --git a/bindings/python/setup.py b/bindings/python/setup.py +index b072833..db37db4 100644 +--- a/bindings/python/setup.py ++++ b/bindings/python/setup.py +@@ -8,10 +8,10 @@ def main(): + ext_modules=[ + Extension( + "ckzg", +- sources=["ckzg.c", "../../src/c_kzg_4844.c"], ++ sources=["ckzg.c"], + include_dirs=["../../inc", "../../src"], +- library_dirs=["../../lib"], +- libraries=["blst"])]) ++ library_dirs=["../../lib", "../../../../target/release"], ++ libraries=[":rust_kzg_zkcrypto.a"])]) + + if __name__ == "__main__": + main() +-- +2.34.1 + diff --git a/constantine/rust.patch b/constantine/rust.patch new file mode 100644 index 000000000..640ab11a0 --- /dev/null +++ b/constantine/rust.patch @@ -0,0 +1,71 @@ +From c3d4cb77f5a797bd8f454a0d88e034391514ebd7 Mon Sep 17 00:00:00 2001 +From: sirse +Date: Thu, 26 Oct 2023 13:46:19 +0300 +Subject: [PATCH] Patch rust binding + +--- + bindings/rust/Cargo.toml | 1 + + bindings/rust/build.rs | 29 +++++------------------------ + 2 files changed, 6 insertions(+), 24 deletions(-) + +diff --git a/bindings/rust/Cargo.toml b/bindings/rust/Cargo.toml +index ab1f5b8..44e410e 100644 +--- a/bindings/rust/Cargo.toml ++++ b/bindings/rust/Cargo.toml +@@ -1,3 +1,4 @@ ++[workspace] + [package] + name = "c-kzg" + version = "0.1.0" +diff --git a/bindings/rust/build.rs b/bindings/rust/build.rs +index 692305a..e874ccd 100644 +--- a/bindings/rust/build.rs ++++ b/bindings/rust/build.rs +@@ -15,24 +15,7 @@ fn main() { + + let c_src_dir = root_dir.join("src"); + +- let mut cc = cc::Build::new(); +- +- #[cfg(windows)] +- { +- cc.flag("-D_CRT_SECURE_NO_WARNINGS"); +- +- // In blst, if __STDC_VERSION__ isn't defined as c99 or greater, it will typedef a bool to +- // an int. There is a bug in bindgen associated with this. It assumes that a bool in C is +- // the same size as a bool in Rust. This is the root cause of the issues on Windows. If/when +- // this is fixed in bindgen, it should be safe to remove this compiler flag. +- cc.flag("/std:c11"); +- } +- +- cc.include(blst_headers_dir.clone()); +- cc.warnings(false); +- cc.file(c_src_dir.join("c_kzg_4844.c")); +- +- cc.try_compile("ckzg").expect("Failed to compile ckzg"); ++ let rust_kzg_target_dir = root_dir.join("../../target/release/"); + + let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); + let bindings_out_path = out_dir.join("generated.rs"); +@@ -46,14 +29,12 @@ fn main() { + ); + + // Finally, tell cargo this provides ckzg/ckzg_min +- println!("cargo:rustc-link-lib=ckzg"); ++ println!("cargo:rustc-link-search={}", rust_kzg_target_dir.display()); ++ println!("cargo:rustc-link-arg=-l:rust_kzg_zkcrypto.a"); + } + +-fn make_bindings

( +- header_path: &str, +- blst_headers_dir: &str, +- bindings_out_path: P, +-) where ++fn make_bindings

$9U(OKLj0MtiWOqryf2L?iHd3`go*TxCvWFt-isxDA)VQ zVDWqTyw{1YRh{`(oR!^M*KbqxhP2L7z5jGAM`8@fV+MV}@DLT}z~Py5MNz z%lQ*X*jFK>Hil5M^g9+qn&xTxf*;Ca-ifESn=g*RtsJ?Ks6=*Y4ijHO*FaR1`_Y%0 z&NHtDIDsvE+ck(MxqLvGPxg|(|KAlw?Yki+Rc<8UJ7&aP*|&rX< zb? zXhHzm$Y&PINcoCUB6bp>JURZb3U@ZAR^;0j&ODwQj9e@seU|YDTsS=lxWn+-9Zjx> zCYq9kt&0of;3DW+4LsRIG%`%wC2y;41f$LLw=)xxwA`!6%}4fl-0@ zVNo|mYDZiTkF#BE(dH7dpXC(TmfMdEWGU`u2fjCxAi}W|Py1Q=+$RFg_a%S*BI}Tc z-I))yfs&uaB)1LbDaN4+wbZQ0p!nBTW4;HSz@mJ!=3NQ@s5A2Q~mo99}4i4_g00752+U%32l7SYUl?~C))$x-W1b^OyEA;c} zvs;Jcych(1zO*KpWLco_q9DiRv_5k0^1M4?Fv+H0DFZH#tP97r49Z_s+pFxLbMN^3 zg0>6<*QSz}3fR2OYYjw=WO-P%@%mR-ez#xb-EV3;QLIBP;}_f4 z2Meh$VjjJYBXWyZk|$X04$~_N+;O2|d57o@qfxUnk#$X9O5vW!$W9)fKs1lgHk=E_ zVUjTm+EgdB*GR|J{kTJT?5GJA(jm9{VSX}mLRco*04VR=2t=G3-#w(Rivs1ik1r6F z^DN1eU-oI*&(FDf=?1QVZXrs>5U$TL?<^xD#p#}0FGtlc7p6Wf!?|@Svp` z=XSUtIBru^`%ng?U+Lsrmz095b!&6?UL(ojI2=34`RKL+`Z4q5MtI14n!v8Koh|nE zdFW7iyhNc(dE}qZ6pQeUvWT=LzeIiZ3#y#iq7fmK7U;zfF$7 z5%*-gq#T70{PZc>%NgTxW)?WLKeE^;))JrP04AnOATMc7$s^6FtnCfla)$;g8L5gR zM6zqEzl(<~W?I}5if8N5j?IouI{b0cFG0^8CDfqzbxsQg&!6Be(OaQpb?p8h;=-se z(R33%*d<^2&qr@v`&fEsX6j$QX0RsT!7H**1ocJ~T9kmb$-=(OPF%xEi2zkL^x;J zSe}f~fudFPLcNS%E^RO7W2zAZ9b#)4W2lHYO|Hfo{?4AMwDf3YWZ}-qeW%Te{baim z48cpJEmBg)Wq{5`#YKWbUjnbUI4uLd4@T~?4ry>FlV%k_$I_kfw0}r$<@*a?bI6`6 z^fkkl2GU+n*lj8>wPKRxPmY6X20l<%4v8H*8|+dZfOxR1;3y!{%jNUBzLde6bmZGk z?#-t{(2PN|{zfN8MrR6-2q2$n?ryiFx!$;1WmqJH&Hf*UiU4vHR2XIp4PmM2^`y|F zl^Tpm@fUMlwiTdgk8b?^uFvt9$|TAAZ|z&rQe;kq6LyyLgL+@?RhQJazHm0|4R|X4 zwRjl#py&c|eDz~6Xv-o-gB6J8S@-KPOnl>mrG7wgulX(z#4}m^Bw)K`VS+K~rAWM& z<}HHxYk0Z)$|rT+{&~z$2oDQV7Ek;5ukFQB`>g3?P53UrJ0J#vJh4$uet4yPALSAN zgPPPrS8tA{S#eBI`ne{eE5`%3qN6#Um6fU$DNXi$Jx1DcweHlzD!UL z$69uFSIFu}bR5^Uuy+{c4k$?5Tt97q+*hQ-W(S6mFW|6|oHj$^XQmTAj0446+^)_y zvjk2><{G+NmLBXOR&g#aH|2kX%zE7EMWNsZ!VOv%t|85`{brP^!IY^tqOhG4Pd9fo zt6H}4!p_fgw<3m0^@GD^wON<<$8`9u10jkYEp8;qRzDT|2k7JV9JBAnUHoP_kX14Et!9_;|b4?h5t`h1g&z?Wt&+_)Po8cgfXCQJ6%EvcgU z)s78cAraiJ$LIKRvw%xAQRwGO+BygXjU5+e;7Vl@(6?=Lmwdhf+T=f^H6!g3l9tkM zqvQd!C7LR8_bDKGodG_)o>7wFbKkhpzwT2QVp?$B>kFE17F-kt;3rziqGkX>tJ$|e zlor#aC}63TK|V`Z|v@tqS)4>eE70?GB>bO?MV1SGx|myH?gTM z-(f}+N=3b_fil2YWr19d&BjfeYUXGa2OI6j)qD0RxrF*)40h`R)AcfX=l=sS8~`~a z2KCkPx~C6n7w^S-9;D(u)~zQEj(&9!0jY{>23tG`br5vyBJfPN%oUhnxwpEV(kG+% z@;b|XUzZtaM|lg{8kPk3Ff(Idh#)yeP`vj#TPL-~8ln#X{pSZUK!6H{Ou}?BLPwXT z)cAR~!=0n7(;ZpF+5h`*?WhVt3cogtZS2P#+UDOHk8`C22}C1vFceWpKEKy^N6pH* zcz1P%GCmS3Q}!kYas|byLzHw;01qcm8DO*dM(F|RJY&13nvluU@G}!u%CX0iSXdFo zKXEr3*i^&6_0F>u?y4rQly`~hc7{sV3y`HR+Cp%WhpMQ33(J#uE|*(llUP9(T()ld zAs6_E!iBFTkF#e^sa`-uybqWJ{15?ws+cr7@2WeqIG{mzG0H~rm>m(dc!#S?`4l6h z+al7yuzFVX1db|$T&m?-ZKAmpB4qwY2f6FIT$S@$q_{g%QYsN`Qm)?IoB4nS#hA8R zd%ypf!=OQvs+RzIqks-Exob2U*}Sc1&7uqR>tR8mt^SFnTB_{iQ0%q{p_ETA?~e!;>Wq*8KJ8iK$HDA* z-pheu57w57Ky=FONVAJe7hlx9^cu?%Lppq{`WS~V&d~JhNtuw3G79z{E3aZ7z2_MK zKi-G;WGN52RB#9OqcRCKPy#I1zd$TDTmqaIzR(ie{CGL1rI;Q~x_qC`#@v+*k~8qI zzFwws5Xl@*&zx?`p4j{byRve9q9}E^tzt1=Na+<{Cg9r0O9QkeeuxeE-2bBdzR{qb z@<}K$@@L zdBDe1bN1$IDo4U>?fuNyN?hv%T3Mh>5>Sd!M`j;kiWvm0Q59rOZAVDV{D2@25IjG& zmC_AfB05&G@y&J;ZWM=y7!Vut*UfDWy zNs8C&*T*z%AFZa_w_+Fb1{Nb2@{{Bo8d2@J(jeh z3voEB8Qc9a&&!}vY!Y!~17bUcHRq5Z13#nJ$xO|W&TT!MNd9ea0n1yYDyAN!P2c2b zQ=2<&+=VKx*b3sD?{9_giN_av9gV8dIyeT3z6-m!)=AJL$c{&B4xF^EU?|_B?GL8^ zG+|SY!T=!qrI>!?k_n?sdk^Z45tCk9G^5>K9bYC&o|q0J?Wt)_LDoKItDg#HOL*{l z=-@p_graoCfm{X8KhlGRN|X^Ru9s3&cG+jb1JYE1MrtcpHTl}*dRQI_{6%JY$yTkx_c0U_X#W|_($NGeWP3gm#0&xw6H zT>230J7m7F-h|PgWw=D$H?HIjt{?S+zxIF{#LP8Ick=kKJV0*MCFVP!=ZgH(!ho!j zEFFzgv9*nWQ>>2Avk&=sL!;}+uQ6CP{WzVZ!5w5QECnxqW3M0pUo_3!E1KDE z7FNqLdYZn6q{h5rV4%A^wK|{Q4lw~G)Y~B~FZGTh?c!eb%-8_KkQOHdQFK%;Et$@R zZ2?v4j}HtWn$8ftb2_q$hvM~naMMG+P|Kl7KWG}3-ZPG#EkT+;Jj#3vj)hyE1Yyo{ zz;0*;fBc|Zm^6qW<$7g1-^<)ZBTkK!FQ2w^karIxIaKRhPK_VyY(Pe@7CHR+QXT&A zA&*T?BHk)(@{RerQ`uy$3ycO7{-b=@^s=t30FNTC2jP39G!{NG)=r&r5_ZByLvTeObxRTzuB6U!bLwogC$rKR9NKfa7C8+2VWPsqOU`7Or%tF`fMsOyVLFtP2 zh{aPd9gox0@Jv@T#oVcAn;bfaBf7p==@He-m zs}i8J&5H)8FoW##wh^-WBr0 z(rWpIpZ#T!Cs-Yq4uQB>(7*<4WlP4stp zR5ySMEdZ(gp8a`@P^nCfx9(LT!2Rn`E_pZtXKx6_Tp7QL%In@J`SNk8@FIy4!`ca0 z+!o2le}Cg2-n~3wy5c>%eaHhGn5Zu><6N2O1ljj>sMK9L=%?d^N*eox)JC{X!hPyP zWa|{u@;PG;;wE;7tJd#9q9vW=a?`e?ID7H3n4~$0-f9Ep<*N9(_l^)xk6$N}Gv1wo zdAx%?A*m=MYn3P02whB(TWccv_wZ4qrIw!9!a&zX&*6V=I|QV;Fr5O7E<-Bo(4>e^ zcpCq_z3+2N|IHKULUrs@e(R)O9by1Bo&uz93e?w1e0dPG6NJXAyNrzEnrS9beR-v? z&`ecj;|3V;<{kO0^ox*}-p!Qq{5g!+7f&eJD$=nJAaM~jC2W^LscmR2aHr-+WqA?J zL5uLm0%0lyat=;6fRe8g#L6a5^d|qJ{GbTsRdLkVJaseV*3keaQT_UBzvBI-OPB&Cu`6n}>}8b=maj*pwK=CB2P55?kYn3Du^&o!zf zDIW7V>{*d3v{>ciH_#JZ^!^N{Qqvuet61i8!~{c8%iFQX%!Vy;#*XV7U+6-%yp8$I zirL?mg~V^QLHlfao!5!9zQZ5XY$jsbqbAfJQ3>i2>D~B20%`&%Ac}IIjOJ zuQ;u^92mpdeQ^m&D|DS@mG#1|Be>W7mu&HAT!jE+w~j0Y;dDgO#${Nir}sSd!Q({j z1EXELOCF>|Y*a|=i)Af=JPCM|(em9h>5Jan#vBb^G|ZBBM5g2Ykh_i?Fhud!CMR5` zKvo$=+jMZei(RxVNtqs(Bapx)b|E7W?*Nab!pT=|KI44IxWsItB)NL9+}{HapbW7~ zQQ-MomZZ}HKeOr~iklxiHVz`?ss*T%s|=jm!A@c)i?8Xlu4tl?zjGOt`&15$)TdY| zz>4PMIxP_9Tp3^W*h3p%6nJ#Kfi`va`r)@|9&|%=OZmdh%Z$5-I!3mHs6QdrLKdIV z&M^xF7hxhc=A5)I?ev`56W-z@AbFtNyloLrE3yg z@Ce#IV9F~EPm3vPrh|r-4F{;6ZEqG~*t2CL?!N}cAjk2^+g_0rPBl)vKT9QtLl8B( zOb1+bl^MUBWK+xE^{Ele-CeVI_o%LNCdeIA zRs66@`EE;(+$7?9EvW|x6xtS@Gg~H3Ji8ErhvYq}w^q?MWK>tJY$2{A=7O&By7YLy z56yl`NzrL8SL&z*QiNJv>B1R&)zUBzZjNNBMl|r_uWSeOn?~0jddNEnb;SZ*q6NDC z0J?p;qIUpir$4ANRTnm$eRu)!)*SE4Bw)|4=^nzw+2y?=ybZ*q-J@_?jCqM|?Awu^ z`a!KDIxNqS0Rzid(mc9Uj^a+WZPq@PDg~0xbOge z-21rHH9@H^yD8|l7#cd_*p=|*pC*{*WfMmN^TMy{dkG#@hfE+Zf{;W6>H(PI-un!N zVXBQSl)Px=Noc2Fa2-;#U%i%@SnG1#-e`HTq%>$mG1`Mm^PC=>dyc8Ufx)pVcZz$UjwKvHINIzJCm zh}z(s=$St`W_sB&!a#2MuO=2h!FapudOP$#o@cJUG3L2aBi+bq(Anjfb2__#;G0>s zI7F}k5V0`iPDHVO_!*7KpwTq|l{3)sfCfVG<8wvjg@y+cVwK7>_is&q22@o#F@4#AB52h6i5GA?qXs8+-Bo~ zZmFzq-MlK`l8Df?Jm8@HhIe~*wU5@z7?=zbVmuDYRL#d`gpzSmGSI!&5jSDOevDYE zEj!%kiM-AHrqvHi<{hy>IZXs~f~Qi&IEeac>IvOE*T3l2aiHKL$RP=UC{RJ(mZJGP2@lbxio zlajGRCKrK%&up`er8}v|9`N6-nG{;q5d87((UQS+O=)Z;Qy0o<2RJq~f0q@}AeLUS zAhjuSG^Ta|JyB`SzsYxS<5#RiUXs3R$h_-2$32O}x!?cmrOYi#W;(UKNB{*hoeHs- zlX&}ku^yXSSYXlg{Z^4s>tD{Bkc9r@lIy0MUJW6W#XDcBjoA(7D7+~9gUZ$f&Yw-= zpwusDk{t;HE`4-#27oDGqg7c7CmRP{wgeg&jaORiVNQ!+;;;qLqM06afbu!XY2oEXq5KzzPnLVp3s&isfz5X>`*5C=8d~L8TMJ zhxdlH0WuJtJ1KZIr8uK0Iul$eGw4MWfc~h>!f=FS`SDc%=lzt|vNvDYPNgF8ayM{< zHU);Lx2=mw$oFDBrS4$X2wfFXZnzSdr0C^K4f*hsLs-jxL5{WLcZ0SV9EWMJp)CHk zP9DD8xypXdl-)F~2tR7(nbn1)I!g=gEDhJ-V@^m{;k6)M}?G^y{wfQm%?STdLf3wm>+PP_%){w7_ixB#cCyP@?XXW|N_AGQSSi2Ygppe_Bo-8W}#|K&RplB4H zwxfB>*EefPiyGuumOw2^0`?UTR;2wV|8Ct!du4oC;W|EUgV-8 z`pfm@I$XbXdW)C%;}6vEo+dw3zjbV7GaxrmEp^d)3?ym1QrTasBZC6i9WJk%&ygS# zaj7O5MxyeX?8zD-N+TaZ81@EHUlXm8WD~67CEUd(#Q&wynkaz1p^O_9zPRA3r^g^{ z5omLNm+YC6LkNHj$g+0~MH zJDs+@lNhQCWtfhLl{9!!@T8>S4i^S|92$r7Cjh9=W$K>ZcZB-d)dF!%|oD zSf4FW_fiZ6I(VFWy1|n3G#ZyoBaYOoTZj~%!zF-@CW1T$6(@+INsYj(yUWEHk26Ym zfth_t+bByBwVt_PNo2Gb6F$qy<65*+N#j1F9S%|JC&@^=fb=6NP-hF9W}lYGZXYwoP5e+m^KXv|DWoqv08cr%A|nREGbMOb zJL)8~7V#Nftr5;Yi1V<>=3Tl2ez_$nd`rg=aPEFs9Bs7|n!GDm`v=(0kF-cF*h0My zWcUdI-O0RrN|gasLHK}V7Xb=MN*Vw_QW3jq-zy}`$S6BhYb&v`#7ZfmQ7MoKEF82V zKdfpp5F3KE4LJEk13-4uJpP(x0>Is4g@Tt z7p)Vp)w9=W-j&V=JE&c7&m6XW1iNQtu|ILTFj1PIR4Ea_|GfLJQx&(pj&O_l74M-s zJfj`Y90UKaF8wa^L;4X--wFQU$(}=TglX9 zzP*Nlu}_?MN>YsjtuHa(GhzZ?MqGex(E|nF>|#zYU3@y-#5sve&bhL@Kd?>u`?51d zEey>9tsAk*1adQB_37P&dVAM6h+)-#%6uoK=rRU~Rc7sYAQBB=AU!XVGjB{m0IRB#Q7YS2n4Pbzl?ki*QPIj>3 z$83eGU+;BX1~BUu(cZex0%i+n!1Z|2qpwW;<7N-4(nxuPZhqZDuP;7n{|y^8$^|1i zF5RQn8Ek1KRIYr(x3UJZEtT%pD_S|dcfo_X)i=yL$8!5lXrG~Bq~DQdN^neVnAbxs zc9CO+{omoXgFu4BVwk=*1VWNN`o;)#4%v~It}Q9>91&;U?-G39<(ap)K4h`3F%Lm_ zCM;4(yJMk%lppN7r*;n0=_#&Aeoa$7iX*7EYxHUg za7hGqkr74S4>gr6lJ6RQzTspOY%FqfXFD*-a?`coE*3;5RoxVqa_S4%X;9Bc$z1xx zJv7dMWGoO2ZQ@I-sh?+9G)3x3XdS;Ep9i9+;m74of-UXqk+>sf1<(6ueU*YydtcWL zt<1C{R|W+^_H{Beko_a!SSjBWnVCb?eL#Ni6=2ny=5b%ItLQtfU5MpSaDyrHTKf$f zN1wpKQTK+DblHo&9~g?aX!U{CD$`8;Q$DNRT2xlF{c4nF|Fk4 zZVK0=DhuZL`Rxe&Q7Q*Lm=Mo<$f!65iQ)7HdYNUAgFB5hjf9WY#!kqh$uLZ4Exy&9 zTZi3qPqVWDMRRj7x4w{f$%GP%TzNCHfZw%lfcC4RKwFse0HbeNl@%5XO3qRbl32LoL7oro8vOUZ#jh-1X<^fL%-;-2h~_grA5g;tJq^)-rhQZv z<9v-2sz}vN`9eHg5Vxog1+#$L_mKLG0MCo@)m(LPH2Tq=cqjt)cpoJZYb|(r9-ymh z>;yV$?g3SEV}%WFi^TQ=z|j!7>pY3P7$O0S$E%>c|MH(H>&n=-^t$Zb_X*(y%ogqd zRJEp^IV*LRt~Y^gl|4SyMkn-L1`^%$`f)Y=XvmW>cDHh!Wn**DLX&QWy7w!92PJ#JEDR796iSqJ#_FlRjmXI6Rz0?$z}Ur`ILD4E>=*dJLv?) z`mamDhT*Xm{HQtq{R)|dT7U!8>Dq$Kw^VF60iZWBZfPieDg%2!_mhRzQ771 zO^}69uxH5)6S}(8j?0#Zj>7{zfnW;sL@K(rXJ}kVb2c-6=s;Ne;|L#Cljq~Pj=vtD z4Ilt~gXj)Qxr;e4+dI769 z|BugxcAwN>+v^^&Nw@*rKsfW~wvJ@wjXwU*qI#)neEUhUBZ8+k$r%lq^Q!)!fBKt; zZk1mkRl~h+S>lz{vlm4BtON`JFk3WUyEk;r3{flRKKPsq;G9i*!3{n<*;*JH={QCo zhX0oKeD($^@X`ykM#3ePwN9W3s86E;b~2IWZ*81V_|aUsv(Kd41*8lF=gXGYAZ2mk zNvpJnQ(-doXACX@3mc}7R%Gf?$ zh!kZ3+(5?giK*KFxqnjBr%7Wb0#G^NtMEhbs}DTf`kZoUUOYNKj!YP8KZfamyZs66 z1(4;rq9e6rs0x10jCp}05!+G9n>ybBHHpW{GoFeRMROz=R`>{clP<{V=fEYy$EwyM zj$O%S#TaH-ZKb@dE!PBF!6P9HvV*gXVJGSy9n1tB-&?=afjxpL=|ojec7N^?rZCd9 z8RsbeN~4ok9Plnl?yC2ch*JS0r>F$5K!I1l-Ll;D-X`*|xX?enoR0@S$#KL6G7cUl zPb?7ol`DG!z-0Wo^dPC(utdISjxm!xDleXq@gSp0B@s*wh?wmBAt-0`9al6P1HuLQ zxAN_kII1v4A8HW<2c0vM=2hk)?J^k}rM*$BcchnGP16pqyJaVqGFF@Ambqv5-e}vn z40GOu=1!sv0}hFx+8#@E!Ou=ahx_Hc-0EpM*p_%ep&@C@ekOAKY{Iw9sm#eg%epZv zD`X9#;BNdJO=V^lvm_E9A9kPEesfN zn0wbt#Yen0^S_mVWq!JXX~V^<`)jq&z|h^@`8UH!&Ze19UQuDZm>andTc?#rOmsY3 zQS^xKs0-~6i#x@DW&~|S@Ob7%$Tj`>^TYdy<3kCTS_#LIZy}BU!@pskCBXVP)10$Y z=b(u=JA568e;O`~x>`Rxos{(FIAWx$W#vX=SdCTIgN6b3;6^Iz!I7O+*ElPT5>;IJoL5pV1MNH zDPfyz6gyx%tzsV04A>nzq4kwoE9as@TrUBYU*DgCjOEa@L$dnH-9Ii$NgijUcnK>u zY`2RpeRcExu*^UV9=8eDJAFj{+zKM{&xJ)5>Bx4X6GQA;pPca`+&r;NLr0P>i=+Fz zkwNu4FgY&tU6!Gi*4tc8>=p#9{5huMmfItdlYWI7E7WbDJ_;;!Ns?O^i2#3I!ciW# z0?15)Oy>lXmc2)qX;Ganf{b4^Me&=xv4({64K&k=UpHq_+F_+W{ShmCswUzk(Tw#F zyN#Akar4ackrC&yX(InzHwn%e`dzJRvVi8Q!&R0Zw3X=TXF%6dA`jkq8Y|kglq>>& z6Du>1s9Of1Toi~;gXO4jw`Fj4h^XAvu$l-&ogQ(T4~Pp#{5!#0JQ-fA*k*|N&MI8& zeGtduQO051)>kOwQ{oGLX)5K!qBnuxgh}DNSZevoeK+umm{g(r3R%kk-i|tOb zChd$Ch4%>tU!a(Cdp8x|G3*!ZhhtCZ6R$s+SUi-Sq>yqVHNcBTLKv2f2@4xW)a54~ zQ@hG?5pP)T|#ioVgYse*Tvp0ji$HC~FK$s;ujb z#W=0`A*a2`p-u%xJvFlUZr>4r8ks(2jWv~HB%zx=ZEE$_IRiyt-r16Mc4YCW?VwE9 zXKbMSWsQOg0?!6opyuTL7aO(`Lwu+>+Gr<{gya?tD@tTt+&SvcnyDIl?QBOq(`x5{ z(pKgI*QT(4wSE%gRT^X5Sln{ArFcKkf=XTeN}wyA1;kKjxnmLQT#T0xF|-lMHl3Sh zoOA{qW+?;x&|>Ba=i~hIcs8RHbnD^l2=rMIrIo3?Ny-z?7h|sO8*6&|>><96gWZLJ zT$D2m<0wwcmjE%1NqObud-FS4b?J66Vj~Zx zEP$650VjMngn2M1xVOVqzm8EENV=}H5d<5v`RLIe=;U^s+8)2~y5x}2Uft>=?Eo(; zCgj=fjpy1INAyZd9K{1udSBG5;P+|Rk(=E@2z#diP%72=(w+qG_a8vpO$vf+Bul85 zW8Z3DXmU+fvOe1@*W4>|+*AN~Ecmni%B*G>qJ1)Z;y<_aO;%<-TFltgD$}$R=M1vm zh#gl%Yq=eJ&J=R@I1SY`Y;qqkm=T5%J&6ykVkiH&5rK^{(OnV>Jc8rkfz zry)zxSFPfGL+ZSNxRX$y zpaGVvorrr40HlvDdZ)&1iJ0Sr0IH;@uXIQd{mg*sqU=~>djzLSYzLN z04p5dFW(aLA7Y@e)W3_SddMGs5hFY_ovu7q*Xuj9fmC%G2X0~Ydh$|J;X1en;JNsM zOsRo*_UF5k@|A_M;15(Q56Q&=I=u^EK$%G+q7@@3|oru}pEIG${oAOwI*ftCb~3_37Q> zZYiaOdc^A9R&3yX#n7Mv*X0)Ez0d>TfX^dkXg1dDK*A6ZJ_WQzxW528?zi4AW9C^g zVA^w`A?IEku|BWxukF4xN~lsEmv_AWDIB*j#K0Z3Iraon@=WYd>XJRa7mvT3z(Y4a z-FYf%QR4v)-OYrDob`j2T70Ve;%RG_)*0)J48xPFB3DFA9^nRTXtz&YzO+kIrE#49 zi*z%Sw&{#|lgy zNZB>uVOKi_g>Jp%uRE<((0Ye>ce|O(=gtM&FmO^oc(YAS49CZQ^va_ipCs;uhI$JN z%a6kdu~NWxXf6`gqz2XOiQUrU_v9!%a2+*Qr~4?drj6S|vf9RQ{wRCc`F z4X;EHzqt9L!2A_#wS7;yMpjOUnMM0p0O}^?dNNJv@6E6n4?ctZ|AX{uw76Xqixb&L zf02>qfAN4`AFOHz-Rh$k2XS@BlL)y-FQG5NUi_+-xW?$Ntyyx!61o0M#(>_okrk9QwjoZV@wx*C7lpLQY3<*dT1|C)-8|>Wex_x zbZ(sfn+fk8=vEWiqB&ayjcAUn&CQnTQ@o5U$l3n7(?CdfQWDb}qPW%9174a=BF-$C4-M&k*SW+Y{y_?xYG)}Muw5W{wVu+Q+{Llw6=DLl8b{NE1`TgfE%DRFft)$> z?hoC;moZF;qHUQJ<@q~xH28MtQHbXXmFjaeOOdy41eg4g)xO2N(Z?Xn6AGHH4ho#nEfSIf#FtV3mqO}A>JU+7wyxEE6pQDFSWN?Msw0urq(gVBZECd&KhQ* zg{-6_fn@tlLIkHtl!E(SKFs+lT3Mt7^U~%rBlY%4E=BJT^VxQ>C>F=n1A(TPDui=$lLG;a>8S#R*+{S*O>3S{45(&#QEL^*mrZ~x+*(mf zr6bV|I`0>iWQs?M`p~-+=jCI@o)@iKil ziT=*mn;>g%jxmB)shyq3lJtY&A%X5XA7tlU@!`AK9ne&M2W_s4Z)5aXO$27O(2NVb zbW6M5G~)0Op0RusO?Pa4FF?&&WY>9}W!d<~-myK;wvz@e{WrhXot4)Rj%(Z`@imV% z(&i_@kd;n&NY{ufiU{dR|B{o`L4KI#(>}5jqI0?FSa>8?ht95){8n|;F0-21&!>JT zLEt>i0e+{Uq+prk91e49pEEm8$s{)uWxWOLZP-$LeD8HkR7Lc!GDZSuk0`d4)NtK= zZVx%gCV$*dr&cMo(seF;P{Z70Dg>8u8i=G1l=+8oAj$LTXwNQ3q8`roe1)0ZP(7-J zz&bvKGTd3v;h{Tx8pdc@&z>3G+KuQdCaGVtj2Eecl$a9S3};L!TgWfGKzo2^?>-;HLqj$%SWC7|UK0BM)uww)r-1+{L%Hh5u`U z0|OLw1hy|r$osbE(rjoVT0kr|$N))R0c`F~0+axP~NRCh#As4$dJGBkAwKeS3 z(qQJ<#^%bM5~~fZD^y=@)~Rg2B4g9w*zjL7+uZ|CXqrbyoe60k!2_9lPR6EITmK?q z^*PYG3;o0-uBtMF)xL5O%M&1dyrmSCAF-%0<&F^1DHG?BRD}j*RhT$^2k6q5w$XnJ zgnCq6tMg=n*h#X69}%Hl|9#1(rQzxVv{t3!0%BFTT8E}Zw__kXido;)G4d7pZZ+Qv z9uo^hbmBGZhm#3{ic;fsy6EhAUBEFrtPLA*hG!Uib^j>+d0*t zi&Rgb0Ih(P!xUyW;=h$0dYPYbK-YI5pf9&KkXp%T3xHQ|T|p!-H9?Wq@rXtG*&~h2 zG28&WLZBoG<$J6@QUQ|{aE2Bw2s0jI;$-E593Afhf z)nMH48uAke(jb2AEVPV{)(w;95jSd%_}u=SdX5O(%4n!=29;((BH}*s7#cX+;ISkk zPGp*VEHc$3qXNG%iYg2=@zVMwEvpcucksinynL^khwb1QR*(_=y5FV9_m}6j_yi1h zWLa4pD+*qp{lK`n$hl0)J`tkLR16teAO#?Y*p9=fgKcmj2#<>U#(5}7;iFCOJ|xN} z56I5`j^G9qgX5{y&@4l+$)7>qk1asuLqFgRY)Dyd?Ug7P<#i%;cTYXbHpE#*KvyMs zm*bB6;I))TZiWY*iM$N&e~fmU=f-EF9cMJV61vA^(#~*OS;wJVX3y3|s@b(29BcrP zEJ(a33BCrG;570Reh>*ihTKd7{mPST8nM+=i*2u%b1+;y<7A@vI|$`z>(IE9VO?!Q z2d{Baz(Z?bvDdFp__y&%GSfW~2X0whRo9(_o*0g5Tx@`o>HcyPW*HaqRF3Ng|Osu1Y&)8Y5!9sK)SX z#9sV&$C{PA_T~zIANyv@LyyD36X&Q8mL|(*B2{;Pj{E>h^}3U&AO3cw=nsn^>(uW? z?jcbrP?upOoVxADT>kgy2sX%agB1)()1)Yh=^3(PGhrXh6|>rn?e16IH|S&B8rgE{1<7Q~m2^iFhvKGZ7`M(25Ob)TJb=Tx?`*)e5Y8 zLSL$~T3)mF60s?Qx`I|!_3Ey-lA;?F<1CyjN?RUhq9~}(s7B+)}!@rYfmvYv}v}fbH}Zi+l+aodXqL1TQwQ zm45KBR~JMDJ;U^*C0Ve2Q9;|+(t6u1$%wccz-)e*gP;5*mhZzwbI7)M>9fQi9_kLr zOOzT1Iw{{)RouyI?`Cm1?M0M;7-Hc#TZhq{E6TO#EVP6lYugDc#(t8Y5UN?Tn-X>A z6!-=hh<;zP(RGFV6JaPR7`1EI*o^1eyA%IcBBw{~8Fi#$`wvZTqZ{nS;O4)fc4old z*d30ff~r4s%Zsx9LEyyNxRXKu{&{~qZ#rk@u%8${sVd-ev^|wk-0req59C6It+RFk zbYF6jQ-nQKZi-RRci}iU$wsD_k0t`7gBgNo&&y5ALpOe#;p9^-7hN06##$zw$U_?^+x1k}!sWIPVZlv2lZ1D;~3Zdiy>E z4)N8{EYq5~nZJ?Y^8?-K(kbv|6 zl*K0Cf#tlmTM(>vfYY2y0Ks#E8Hk7zm*m6NvoVO|WSr3KS@IwREqUCLCD968QGys^ zr@^xqW^V2HnLKLqxJ?s=VLjKtf;HL68g%2o1HmFDah1`f{egmTUIM>`+Hc%iyKw}w z6Kr0U?_u^!FzC*qMK4&@bM1aLZ2EY_S>LGZLC6q!^{5kcvquC5m`ltq&vfzzG@#K$ z@c>%hP#o^RS**Z&fbW&{NjFJ`(`Gn;x4DDT!_vjT`MsglQip}-QAC1~HLX)7$}Df; zUjTYU$0V7bI5orE9uBwBBC~3GU`_K&+4YBuk)$%BH-MawBHUgWmBL(9KC zvL2W^oU^CtlzKA(Db;2AtBf9OnNB+b5q-z|W4XxSw>=J2jo`l5aP9$_^0g8gd+zAz zf^K`W@4AH_VA^3eOxZ2vcd(eTeCcdYVsvp}5B8}{_UQ~w4o?#9j)9VIp5aaA>V5b# zb8IO8(lFF5YMPL`pCaCPL`wzTuX<*nL5lJUw7gIHt3yt8-w5C_oAm*bRAult?wkK-= zc&qs|`Y`ddm~}(&iPcSxN4m)O;dx#)se9j=$^JHnl$gUE1YLfN+-Hpnmal)Ywu-DZ)czF{gk?0cJ$_kb)oVkI%LT`S9}SuaoVV+QN_oM&=}x#k8t+ zK);Ti9n>c|Nt+a>#Dbxdq31KA$iP*_#f@uqfuhknrKWl1V8%_e!3m(@g!m4$R|uW= zL`?!NGS4S-639j67YYxUp*8KDqfKcBhxzK>WIGU0{t~beeL>=P5v{blN5YHA(|Q-| zAoWNjV*OWrpdmoyj}FCM-JIjg8Z$u0-PAcq8n@1~TP-6?WZzQ=@uj9${bj6R2!3VaKKL@;tY0?*Z+hp}>Z zFcS0!Nb6{u82!;bSZCEuV!ZB6JOL1#LQ-Rib{J5Lq0!!4h47IC%N+!9E){q`I=uCQ zbVL#G?dpJp$&USi%}wWSzxdqw_*D(Q_L!)WasFY8+y$1v%+ZuTy|;1l_t~Hk6gAnc znb?;d2g&hoU}kjmO~{Pgs$8!#?OsyR_#|0oe>aeYYjsGDH=lL@vwDh_q`|{`#6>lk zwen3yoIC_th{}>a(5iWJn@ohxjYgX1_0uS$HI>r0n8^8`qKSn8NrF{5oHR4u_?md_fVw)^sgi@=YJ*}S%6grH0aaIaSs$f*p*LR6GT zTqBmxjW(GemYaZBny_c&#Go;{age4At7tLL5PQuv(XWyo%1+lLKyL+;k|>ey-8

%Hx2ZAIHnts4G@9v=&6?dC3lcU``T7g^Wb%v73478F)%_g`hKHER7 zCpSxQ*<(un`}z=xgHEU`KWkfKtW= zaD;%6h#AeU0~t%(y1j33+}^sX$1&ve)%Y5QwB6Y+rkN?&%VJRzf=`r%UjR=6wD02u zS$C@BzErnE8?KZ%Ix`(oj2sHKZq5CX2=8JC2!GlwwG}w|W>iqDC?_8R~ z#{J*q{&P%_dMKh|{AEB*2^6;bZTr5Jc|sSXFI5aP;RfVyS>H4qCTVX7%^T7Ni=q7V zWpXqO^c7JAH!(P~?Q|W^P=``e^Rv~=<=?W;Zt}?(HRo7sW?6BKI!wS+XgVO%UmGa> zKqmC+JUTpX>XP3Xu=DYeYhn*xCy*`&NI3{tS9C%-V78Gjs`S^B z-lgZ1TA|i|m6=u)1$9|8BSgfZ!qXo2{rp$gx~gDbUDZ90y~DX(^i)qPU|QCrYb8eRozQf}ILrUZrUa7g&OL zq6-jly$xQcN3l2!-i(V-Eu(zD^pb&zmk?VoD(yF>X4OnTxt~hb6A(n1z9!#kjU4~O z@iYt1!YK2$pIftXeu4H5CJwg@M-o>^`GP1#W(85_UrtTy9=u`64f!$*ft##DoQK!A zutX&8pJ~)g92+D0le(m9%uM*+5}h=i7| zF-ejSE0T0~dnVQ#uTZLvmk1N}KihC5R*QhNZ@Awik#~uCZe5MMJ{X~u-5vf&rI|~a zSJ*&%=Z#pLt0h;x;bnhz2rG{>e0?DNejSw9SLnkGP1`D*>OKdTYO%GGSQWMz-wT65 zL-p(7Nk72h@M1h>B-ZD{3b6*bKBh~v%Dv27FgWyx-V4DI6lWJeZR>-NgUF*y#ms=+ z^XalG2=j#rihb#n4xP) zEpMlk=$)w+IotHkh*m&HVC7&Y(tK&C-AMp#2t5WHVbffTw7AzrcKW@$ zwOFTnd|nxp0pk6uboVXH#2JQtE@=wulS1cV_(opA@gV6AAu9Z6BdQ7DnVr~_J_vrf+n4cm3emV(R|*vZMMAcs$$A*syoD3lYz&-x}TRo(yGeGG;pUP%)y0j89+1JaZ zDBmu_v?P&pJQSDYrmf}!CE56=K7(Vee%6MZ5sDE-)XqmV?w=M6>r217mBQ?z3C1;;Ysr9mwu1?6IXv?Kv^SV*{?lsPZ6V7<2QozwQ~=uBuU8;O$h zFU&xaI7@8#DcEwD1&-wPboo=>qJdgP#|n5RF|0`=*6hO%Nu=3SQqd14+0rVV{1IFO zd9)llefX*^HE|MozSy(ocJ#kV^_fT^r3oQH_*A2=nRX$tj8MY;T}s`#5}tX9ZTE+4 z(*yEtmv)JxVDcatbsw?>#J1@AVXko3z36j1!^fRFPkI#Ihsh}Ea!H;L#J5?&oD}n$ z-CQk0|MvA}P>}Us43-e{Mq;R|%^$-xc@0!=oclt=l|MzdSkT3{#IB3^2~-y;gp4d8ts1wIW6LHc!?h|B-!2?YoAbV~6M0r8H26gm z$;6vdjlvjv|3-wjy~r1*GA5cXhX{)aG|r$zY`kugpirIjO?b+PLyc6CY%5n~(NR~=Fwg=IDIEL{)zEP#uSOk*j>{JFqHs1e3bqpYp%!vR zf+(Bd(1V0x8(XMFlM?ai#7|LMHV(1e)1A$^OXZysmAHr%gA8^*mzN=9QKe|Crx7B9 zWNBS;pdI#hwf>mbc@*3-)hr+>Au@v?Ts=mF%T(yEQHc@j>!7p{k@H^Ht>DHp=5JniJs_*jaJfDm5U z#FybjN1&X33u#K?l9-ADH^wQnphauVBgX2Vx2@5)NCip1jXXeRIrByAU&dAylo13} ziRhwzoD7g)Q42u5tzvE-c8$AyCi}<;vOY6fk=4V&;wFxS;%eB zP$|39^_j{5-E@EB?p+ou0G3{g@XCAQE6jh{!*hzR$A&a|vbU(KL2%;;sHD+DRwXf| zd}{2Ws+ZIBOsw5mk7iB&4S;SF;Rur8{Gi2Ve;zjPA<|kwLC~C)Yi+pNv(;Lby(WVK zeXeKnyb+v~&_RQwoGV$(S&h=C)`|#UBEjr#KNWK&vvu~eJiI?a*NXaDS`a{oC7Qb= z@D8tilu#4NDa2qb&>A=Y+XOZ+uz}=x;>7KClW-w03m4m6t0}f0Mk3Y;+z%rgbQuAr zg2NG}-fkf5{{lY$Bu8GAdP@(Pk=H~5%}(SA2e!W-MeZRHcv7O9KsyL)r}$kbJplRCL*KAA)&-{m>xtZhQ1-t_qXs6Y&?XK1FY3 zkLMOhQ^H~YNcr1J;|WOGGTf6hpHMjpTG$qtj&Z*VMvCxkuWtX_3AR+uQr9H99p>t;es6G8UU=rI(*%+ z{5snF;~8&oCf>_CjS7aDcTBhx{@;aJZ->6BL59y;umU;F#Mp2*%mK6jr{jWJ1M)N0 zYRlIF&>y0aXgoWBlKB6jh0bpM20?vLZ)Sa0vrU=K;S!gf5-G1%UN$(v@H1T;HYz^@ zc{M)fV8^g4HHp9%!OAR59GnaZpV2z0G(8c1xH}C+%7U5kbe1~jPw)PQp3FfG0C(*>)~A3Na`c-2Wg=^$Wtw(p zb$wXamYB%x9u3}agoYORW|9YUD#zloI=V*$C*ouV_*k?cZkxKPIM@4v0O@w@zgAlMIY7^$M2BmtN`GDit68 zP+B?!IX1qwYZni3f{9cL-kqePlI;K>GDLb=%{DG9DP+`HXgRdhST54;kHa3F5&ulc zHVYY{X=ixVTI|%LK}6!u%P*~f%VHpA_FVh?NV8{!3j;_8IoeNey8rR-Wdydg-u`aqWhcM|aGFs}$Mwzuo7PY?tS z2Q@r;G5-n~L&Jabom{y`#BUo^(?kWdbbG1rhx?SYa3i2wFW6V_UD0|L^cc9-nOBHC z4!X?qUvmU}<^2XQpsB?-(-zSB(uwd~gYap3jNCs^gnIRYnA9hLX5S2jwgswA(>f|5(yVrwj z=N$W9dDUbkXJ`2$>PkazE|o+~}ix111Yg5*0Oas0ZvOFN9bxk&oo7EP8N zU&>!WV(7;ac>2r;tWIcRiQ2CKlpP8YwSGT`76amxlZdK`ZAXvA@~QY)vOgRAU!U_^ zN-znJ*UV6o^FvzAO^wdesG!RhzX!<{#x-`4M?dYUl7m;s-Kt4)NEJqC;nL*kcY`e` z{$C&k2DAjPyj*Ss5W*NGD9ptmA7N?}X9*cZ*SP8K|E*r5;&3F*$=ZUSagTTZFIG;e zbwqlz;W}^j34xuOG5lfx)l;BzuFvgxW{G<5K>X_osOCAg z{~ZNzT54|cLt>%p5B-FV|ElkU-w;fUhuSA6lF7iokgD^vhS(W_5Ghrx6L2|$k~)S* zdMKnD?cjYTJs0GD{sc&!G^NHP&q4_qmO`k9bm3*?17q5VwV_T6(JaeH=esmV3G3TX z8+D(rK0(n~&wRzU?23uW4#j+>lwE`YKwpGl(3yyHRGKTGpsEbAm<}Iuzq59ok7N$g zfvUIEL?@>-Y@-ucHi$u3q3mkU_{!E+v1&( z!JW30e#N*ThY6{n$LVqsx+kuKz|FV0(&1p2p+qKviSUp_8zJVR5E;mlxy@xoOLjNu z_9G+!RvsB#F-18uw5+R#$UvG9osZB5QSm=4X&$VDV7d9{X^om7Z6`x)U)u$=K!I1>z{c9t^(Pet>Dvp~oO#8x7S75yTNT zw{s24D(N1wMqFHt168aE4H^8+IiEU14g@5dChiw~dQYrKw#t+yULy?30g(UjZ)Dym z;tvaDheQ)GZ->*Mq|^?#kiLvec#at0!q z+`hYT!PEwH5cd*ga`=WLRNc2UC&8YoDMzT;Q3A|phXd#&w09=Omp2!-vGvrtb zsyU9iPSfod@KmJw>yi97%+Ll^H@FbENc&TNV`Qlx70bGpG#mH0)q z5WSxhQwk(}QTZDPW zuGy%(|8dd*x?w?yg14;=04Ucof#;k>UHFc2A+m&yQ|a%9V+h~>tNQa_`zyo^IBBep z)zLP&Ui{A0gmZvG8Cz`*lK%;R;#zS>9MRw;0IQ?JCMiAn8OyD-0df?rBdV;dBPv<^ z)llUbR(lRhdQcT^=N<{3zA?A`eKQJ&~h`~5zSm=ExT z_5iQ+dz4H4WLOWdtLYUL+APD}(ut;-@2#ZDOxCcd;am%s443Fzs&lCduZmfOzbW)y z+7cq95G+D^(wx2PXmN-r7OyJ8Ej{;hhW*0GMlZ0Jz^Mr5zdvF^z%oW|QnZ}sqKi*?(#j9&I9Gry~-Xt>?k7Im2 zyq-K7naM7XHn|#<4tV`St~fNb;L`mJ;B*y1pgyi|aVgV{>NWKlS5uKX%((A1tn>D@ zunX6?`FLU9Nf@M7;fOvffi_-(G=d{VgF5trCZx968c~&!mL6uzG558*D^g(aj!FEA z0pi&GY2{X0-nZ(rXPFxj{2%wC-e5j(=PbV4Ic;IHk%awq>PN>XqbsHHm-Vd~I(f^AP zNzTiMHL4i>_moXu5|hB}vERcu2FSFjGV2#!?ee;-^_(R83A}Y-uZM34u4b;KrR!c^ z^pnq8Serum!CIjOkfa$V;<`;lywouW@b`~}UoSvn-3W1mh(K%ZUz_D>o6S3M!<<49awU~is$voR;?)KmB0}bt zPfp3A+7%BLTEEj-hecCf`-RqyqIM?y-e`Ybrn+Lu z@K~K@r(ulhXcG8Mp84&TY)ZpN9??Re0-o>rF63mp^m@w`tXpg@=|ZvkbP~kahZx$>|A)b%5bUdtQ=!B?8h5* zDJ(oZXMKfk*u!}xRSv+t9H{P8B%KV(#UR!!a;p_#&6bPl#K)JxSiwq`9tOL_I75TXu%CoOn zYXe3(GMIFkmr|-034#Kq^_zzwF_kXyB^v^o_(0o14T`m4Wne6*h5lxO`zPFGGZzYx zsZosu7DlDA^x`!sH|I(bIG~LnDiqN>i5yh4&$1gb&~tM8VIW zvu8J{!+aZL9KGzO8uxH)xL0JOT@C2{v(#rIQOIu!6wDm@q zPycK2Q3sJh4L&W+!z<1&-z2!d)F1Qp6Hfow_Xa?V{u>C;i>-tY?D--=NN8W+{7&7V z*OwjxnS~F#q0NalhK6&%{)m_)FEJ6=Ur6*J5&dU-cPH7H;a%!1&m)M88M!?9pvQYD zR)juLX z=4Giu`Oyqh+Bnw8U-4GdKYFTNT%*FLH<{|EG+OUPaIOoJp7b#&l#!kvsmKca;sMJ@;vrrKN(ktdeUq)hphl$MmQ*qZ#AXbH8 zHBt`1$o^l5Nz1W}QafFSgIPpN>KV*ZxZUlx63ptbL0C;=8F5K){lF`{_HlM<_5*qj z(;M9-aU-n)S^W@{INWN2 z5Y&tK7s{|^WM-dZle>e5V?d!5Y~x?8`(Y(k+cd8EDA!IbxA9rI&x9)DE+k^yXyKlN zZSjt^%M8&CQAw6N1d0RKwC}`zin(+ z#KFb;awtk!cMFmxDVp(IYK_B>2Pvu#Y(#7hK%MjrcE4x{8y8KrN<^^w+>?_Mmp5VY{wE`d<{Md^P5*rx7q z?zXW$)dcECc||F0PP(JCeB>euVX5a!F8z^Y%ap4XPZjbRV`U}x;|;>fnNdE~RS{!l zGIs1GxKQH7jE|fDph=CZkx7)B_i)PlNsU~u>8&+M*{d=*vkHSBO%%};jtjAr8G16! zzK$R$aLxsUh3}jIwE7}NTo zYd#GZ-nfx|EK>Yp{Daz{GN3hh3L?0glcNwePPH-9%X0oe$-Ql|FC36ntkWS162d34 zZ8XH3o>M!ysYKXJB*Tj%@?fdwa@Xs};Ne;L#Z%C53TMjfv*e?TH~RvLXT_)#NP}Fo z)I}(GAZ){2I4NBlse*e$@Fo9^!TL!G3$wlZq@UKvX!qeBr&XH)H@^&3+HT-D9L|tE zMW5@Vxtn{z)Coh_H5koa|R6iu|^0jBUsmT&CD8epgm&pE1D|>BzxpenH4>X349&g8mTPZ|A4V#wvyS#0{x2>) zy=_NCEEG)Y11W+;t0#PUOx0xCI}G;>()ER>Uv4dez=GYd9SLlY#jJ!>5lg#8iQWwA zd26Ze9~ygCuQ4o^=Q!lJ_HamsOfFM4L1N@X2SYh+`>)7?=M^|uIvu5~{Q@+b(=)W4 zh753>gXy#q&FRh!G%s)r^Xq(}2;ltyTx7*9`AEs|Gsny${IL@(1z>KQ^G+a< zKmC5L+StbK;WLQ`dz=>OSn0sda9=XA8r#<}bP{?#!!QcP5_M6QRA@=sJ3|^#F*UIk z0I0Iqib?eun)>S21C}Fh(mvh=6G&>Q4y2grd#HT-(I?oeBD8I(=Z0h zjFP<3Q%-#(4(4Uf)c}WK3f#7Prx*2@JOv-wgoQM75gA6`RN(#0{8}%bzw4#92n1 zcytAG+AMgJ1M}xshfGS8sf~vp#xmy?%0imEE*nd23d#z@sZy%y56WY%S_PxR9QOp)^r z7Wr;REnfJ6pCt{_+!Gz~`nonEZ^eP)tGYH=00tj3z}3!8ZN4ZWiqwgq(!ly5s&NCm zXe}{={;-vICoz#Nr#{jNPc5SGqq6d$2H-sgXOJy4s&kc`22OwriJ68WACi-pj>SN? z;&QQpvl;jTfmQX-h}t8Fe=NT@3>wWU%4AunBZQ%?1=CYOwlYmi)q(y`6Nl_+2 zO1N*o~iNiWgJMO-=$f!L8;Y_$-pm86Oin<@XO}ke|SuV&eSA{*Z!`GHjHi!Bb*28$q zGrtAzcfgEzeI(sNsq31;>Dg%v8|+UJUu z0^P``&?bn&#e-h=6`_d;?C6FLn(kObu$+IAczfy5Q#=OfLP(ZtA<-Ea_m&{o;_+M+ zfA1IquAi?s#8yRJzcc^;wTpt zIC&e+)-N&R#w=-QyRf=qUwTWY_*y{bmpfw;dD0j=P@84LdqDSh9WLl_(H&%PaA9ZC zubWvAyQeb-SLBzr_jt-un1$khaTrv#5GhTroF^~4+^`R&mZNN7F~MC{*4NnP;nJ8#&O3i5qxt=q9&7Llh#eJP}nWXJMyF2@SNAC}QFAVW^b|tBccsJ=$IAr`#O2>CqJjJ=Gu1N2Ed0PTOwm@LoP zST6+<*B!z$n|R*KPw00;uz5I9=P1ZT@!)1z{yjbt)kP!jRxPH4w_2>fXrrZ8Yq?;T zrG?hs_PR&zCb6Q6$#M90=>tZKpz))q$^D4^!UwwKurV@b*iAjY-~%>$9Qe- zdnGY4tv%!cECkKQ6cnPYhSTu_ERkSk$Q__TsYbirCWLxCl|*r2i1#`04oBxkmOq8_7pY~XAN*(!mT7)&%1l*7ps+RU zbUb=#Pwq8=7@mxhe^uewE9plo28n$;N1^1zlESR}>bC`^5)>#5^(LYQuwtx63!SV8 zE>(@4)vH4RKlAAUJh0jByzRK|H)&OG`-)_?=+Wws;ph%@0dU{syHqc6-kLsv^!l0C zZs!oLvvJp%7PV4Tm6JgnGq{22le zhgE}o*WNQYejB{gM5`9=6XV9-AqW7eZgL0iCw+t8PEVUWAzd~SvQ987N2BPr7L4Cy z*Z+w>keoa8D9IZa@nj3XXDyC zo|RJ-C4JmM!!uP&;-B|Yj&NkU0$`|loveECz?Tj62|3$wOU8$y^P|Sb<7pG?=qUH` zRwGAM6aEqHzaGoQV+JWML`Rh!^Ig4E2P`C}nQtawGIMD}zG#xHLQ&0+TVsH? zBfDTGmKq9g)i$2o%w!#CgH+AH~xp8jE=z*sK!OG=pJhn zFh$Bp#MorUNv36T|H^U58vC+j3XU_O$3QWb>3CC0MFf-uSzm((nTwBx;2~wFPC-CA z3@ZxfoFrfLJ86ZGLWY=nqv+BHg*0hA#l;zS%lT~&qFRvehdfl-bCxPohe*ggIafQb zfqJNU_=#v75A^)XiC=}8MFPI2dTJ5zc6B*I+wtC>9jqo_NtVTnT^P^+5F}3SOTG_ z{QW{8JK;O2WMzvxA-V)Dyu&G=6@V(Lu+0^_q1RYSyeJJ^wdTLk+$-eb?@RRKnm`rE z6R@BF7_Q3>z0nG=14M^x64fL#Uk;TOf>of?=~zDb6UVcpQm)YeLd6|d0*>6 z#Iw&rLmFy7z)b`FxUrz`!YGlxs3+f+tX=413A}DyBaCYLPLBz)5 z(I5r!$o!{-#YjxXD$O_Q=fiXCYQr;5U&$B$zyeVAlCmkYIzLVUR|_JmesSc*GTr4| z;b^n{GNuD3Gw_04kbLq!VI_wtZyL+Ntk;!Zp1>tC(rH4a;^JzSCZt|@&&^hy6Q`!q z&m!hZAiekfDg&6mO}djTZ$1&8+_hxP4ehb}bqd58Y&aCT<`8&NLJY!$*KDXKi~!8H zH$|%dO+!bmk^NY6G-P=7`DmvU177WETL`t;A|X6ha*~0TN5W4XqCS zP`psG5f4ub>*o}sckRolL6oyX;2fBl3fu7Z0RJaL@j$(8RqOWmsON% zXf(x^q>po$1L5+1DIF0|Z5YZAJF;`f-9;@I^Me@*E@Pjp)Ze#{f;psEahDiZ=6Ul8 zcoS&Yaq)eM*r*-6t5A*EaRc<&T&+~fXVk@3QHq(-qyAKQxA!>F&92J7M&-Z%LR=T!oU7QDIpZVswl`rk@GQn|+5^7&yF;=3WL8e0Ec zU_2~EcrAx1gW@dqTLBSXRi)1ygH_ew9U91Le^_Kp8+UQ*2>FOHoty-8r({B6j@+ZfNj%Hpej~|Rs7hEb9USandaa;^wVq+JY|_lNKWDrYpI7Y!pM+QCSlAjLEpI`1NI=YKP5F1 z7pXY)03o=%J^w|3PU?Y=0Dse=xa)c-Ia~wNK=f@l5=!aKBP}sC(Ol&Qj(oFVCVEB^-8wkh zsok?=u+LX_arBQDWM^y3_N8*{GOGd8??U|cHF(jCAl;OO8U??q4lwNrKaS=GolRO? zw7JZ^HrO1qex6LZ=eZnW4cXJdEUx4?gh56KJNMMF^fs9glwu>%+^`wDi^TG0jIbVpnxpfO3RxFUH{X9MQ!O*Rm1qb)vl;SbsIL_4_C^tak(qxnBD|nSZ?^KfK|U`*7PGGLFWQ$68{IiKjQ90JT%-7lF-uYXC`xudKnQ{JK8LpEfMmT)y!o z*9ONtQuQa3C@VI=xrJ@d>DR(6)i>oTgI$zROPC}U{=~v|X2iPrya!ooyOmuI;graO z9Ofq$AKjO8YZtu@JpS^}+12RO?pNnZm5afz;|SIIGhaXJDo5}S?ht-ERVi}@m&%?4 zFbX{pm;;~EFyKzYvdBRDJ@ptexhxx9E%#nJq`;J*_xnRuM(H$&wv>@9rZjQ>| z?Euh_{B}K`{{@vizFvw33##m-OiSZ$sMW~4m0FiyS&!;@20#t@2!0DPpA$YuJVxS8 zHK0~3YxpnGwoh`b1(3!u?wNXEQA9-6#Z>Tde+|OtiVHa9y{~BHtV5)@P-=2&Sv;jy z`M!4t8;Ido52ae;eiCVMKcBCLiETnnX7vk6D6?|t1suIJYIwrMgmS$jIt6_n8e)SPShy>V!)`L<`l^V3l}+uikCfpp{MKM}riIwO zcVxHlskDA6SLMtT$fvtAP_N#RcrR4Vc=S?6C5_nM%m!`&v3mDy=oNr{cX!LM?IETu z`iHR>05g>gzn?a++4yMrUSvl-J3Th-*~5n~R@pIVW=a3xm2GS}|X) zMt#aeev8?>y_b7r1XQX04%4xQAeNVxyx3F;pdqVfnJVp}PkvAbwPhrf)bKgPi;OXp z(60;(X17hmeH&#q?8=F033}x}a|r~AlA4<9MDw;h_*2fbP>|VfNawp@D5kuMF+%<6 z6;zZ@UdR{`qqYzfQ{ojSD}<^MR#mP~+#Og#ToDi~Y09OYC*=;%>~tlO{lC=Hm$CZm z$RdMvT!?(S=O>}>gO-J(x5{a)LQ*oYM*myW!*&CfBZwQhDWkuXy0;t=TR_l3d$Uuq zZZ*@ewi|GyIMQDkaNGLp%^eK~w~#ty&F9Fsxf980wU>25XD7Vu5I*Al!i8X)ah2;p zztF}{um{zQ(MpUg3U{+lfI!-fB2KR6tn@YdF}pyP6rDdi>!d6~YpYz_LM*)Q{Kc0D zvIgJ~>jls4-u`m5`eC%b!lme*cv6_R@z75Gm!jaMU*m7KG#+IEQuu-!hr&n7M0C8h zm9W$=tg$XY+^CEyL(1+>J)m@cCPA8bJ=fR~muPbUMCzpmyz}S006Rd$zqC|i9Rc>T z5s97NhvE0z%sn+thYwUDa8Rs1099YLSmv}wcXbSN#$*4K=1oN*T#v--fHVxtr56E@ z(R@pZzD;w!I%WCmK-)E9D=lIWo7ql1QVGu{0P<(5G|&W2I5bL4rs3fO5`GhWO0pRlBZRYS{(YG~4A&?PU0dS26>FRfg#@ruZwQ(MRaFK(@mi24!>x^5`qs z8q_>Y-n@6ds}Y`FNEc+o0dt!z=9kCKD-hNoGHNR^EHx)(*UcMk&5$L)0@izaVl zsB5=-%a+bG#Hyi>5WR_9Pvz`l?e+iTljN5^QP_nD;tRg*Jqj+Z!b?~j&dLLBR~#7vlBv-_Nt$g30Yr>p{Zfri%s)*me?$brMVnCzqU zue};Z0vrir{4r(xBs&@^J!keyBhoD@5(K0v1)QS?iZ;pw6m+E;FjS^q@ktpc0C?ZF zxWAR$J!w|3%1uj!m4b+5cbHj|U5R9)#9nlak-%BZ4Z_OIsHs3}uJ*9E<-*y=1sT^p ziiXklf-u~}0u8$tbHYYe^Fx#iwAl%({|qKhOxZ-p|=aD zyFw-Lu|JKEQ*_?>$~^>HWe3}{$hgU;4{d1sE5*Fz9qnk1M&Z{;~p zaIrKjo(QGV8YUt>ULEl97W`sETjN(Dde!qoxZtuO%Jyu*q5qda9Kbk~ZyX{iRcOYj zLkd=ZAc z_yJr=0A{?>%-vah3_&EWLb$4|v~n3>Zzqu8&J>NNt9DYyOdnEsVg}5GX6f_cptbs- zmm$Xzl+-h2<3iEho*IB?3C>B^6o7C0uGZnbIxRFfYBirhK*oz~B1s^O+f$PkKG0?g z1d5E@+;&s3LjFl3xUsT+?pf^tfD>|;QAJs0`sQQB^@SJ|lAayb4~YusE?C6d4`dZj zk4p`Lu`hQK0gi`DyID#tRhEn4oS8z;QFQ*WcK+@vMxPI^rX-P=-8$AqO-H9v9*7Cp z18x`YrQ+Zn_~V!byCr%e3yx>HRRy_$0}(5-5WwZ6asC`Af~v3R5v z5Pb?@007R}hv%%eOAGnZqbR5^o8@NRf^2p4>x)cf5bMQ;bpsQ#Xgez%r-3*_qZwjF z8#5aEH8;&`KX4ggT`vI>e{b&`Q|koJes~Iggr9P${8y4>fC2WwKAh z@Mu8vVb^+CyiF@4c(s8+Si$M=D~}QXGcWSM=`A400zfG?dI~KRVA6A`o7#dLk7Y9rwIAgzOSvL#ZJckp@<`ppLWcG;p zq|h{cRZJ9^_+}$rZ}5xwk3=xAol_}%vsOK7F2Rml>OxC=jJ++`GF$;N)pX6G#w~g*UsQtpW473BhqomanpkIK3-WBJI+uT7c zr>yCNk@-XuL4d{tRqKl~9BrWdyx35~Deb6aRa(R={M(0gp1p~~S1owIST zD4Zu3yFtWmF-Ep8CW1#wOe&GE!lTetPkh%N!HzFWnyQJTQq?(60n#;$suMF#)_1>;TFC0jx2q}o? zSh=S@Jl?-XrTBmT7kqp`QZ}%|en_|MU*d+Yt{Vw|jOeqXL<6}v7%bPIywug!Y!&d9 zRB`9051`MxJn9(K7Iy5eSOGYLs1brKn-hFoey_X?&#+jiz@Q4P_ZH5DI0DaH zTav{j<(=`FA3dA_sRc7F=S(eq6^Ei5UG6FwM-xW#+5_l5i#zAxA2TwmrO`!X0)Jt{ zqV!KQJ}1VJ54#);-y4Ug!8DIH_&X9nU-$=aL z>zy&_r9t|to>r$Vxu?N^X47St0Z)l@<7u0>N~z^i@K!fh^T_QiZ(4*_Uzea&=@Kb} z2PK(}eH_0e7)!bhWfSf?%EJ!;409qYh~|88Wq;+ z3bRfubA;^xaH06N#OsB4%T`VhDy;CZ#wa9XP`hi}s_WKQZ zW-MvmO1k$9pyDVTt9;UXbz=pwvPi&{eJcZx7<@E{?g3as62nYq(Dy?`{^{bhcLN(^ z63u8X`r3DdVq{Conz&N2QnM~Twpug2-Cis}4b{#YnO?W2?av|^V~@)=aM#A@a4~8& z!}sJjG~z?oJ*@QiBMk0znQm>0*0IR{2lb)17V+n@K~Y zj6+1H_AO_Ao)MApBSZ0g|DJF=v~BoQ-%T!cnWY;0?|W2x$HSCcnWhT8pE9f)wm4g!kL2+lS08Xm{9|E-_3VI znfKY6VDB9- zkkd1GY{_-}eu@0s-cv1iQ8z2?J zxfmz*_IVkjS;izdMUi%`m7*HSKsL((7`NYavSOFcK6oA>Lbv^&L7e{0tavRLi{WOp zDN)3qLLAw3$rCs%9x+>N_$@0Fk@l0B*U9H?^liC?cXYnd7~bM%%l)(Ypt1@w`|a@{ zbG#jSu9&L#S2i*qGz|ci0@hD+;N;r60VCmJd^akp53&8}3?vyTxam||VZ3Rx;pe=> z7y-rPR>wqL#(s5RjfoQPcO0l1dT85kw16-9T=Ry?Fk0hk;#RNt93G2hooPvwc~ehJ zYj^leO$5096J7SD;-=Mr`Y*QFKE-G%imgnfZQ_-SEwH+c))~2%fnVx;K zRdZ2>Z@7+*P#&L7`U$d4y6+G(a@mvr_3*|X__;qi+1cp_J*C+L?g?3lLSzku0i>Nu+4?G3f;&mzp0!Xo!28BCH;mq0_nV?>9A<%Q2q4a+# zoFKb|d5PB(LXCIJXX()%t1O77*NJNkNQ6r^UF=yv!TZFy7?e9K{5_~%r30+e+foxl z_ROSV7@c5>U_|6;Cv9!Z&-7SV?8=`opwDh3R}>xVYkz_4cD6 z0R?MU99rPzU4;Pr6$;hAV*KNCAyMWS&*^5?8o13YcUbHrhGjTngYc<-=LyH=HxMcr zwA)o45K?7)u**>Y`C{1kIeEn0fyU_Hbmr7*yzf3cF#4qPmFrwroT1r5g!j~}S{?fG z#w+=e6`dl$XWcEw%O=dx&+k7mU61NYO+XdDs7!_x&y$@@n#$jB z!cBQ*h9_-}NQLp11Socs=*1yfr&n=Cd4Z0RM5gkU#MouP&Fel~7O)xxJrB5#ik7*| zWf!ZbTlRjuHZUVwN)A$0o0Q8?Qupq+RxFZfz)($!sVbRlwx4zfFo>^A(sz~`DNI|W4=R1pI4!! zY@q%jg}};ktDzBo0@E%!2E7^j@AeZ0d5erPCN_ri*9-%LH*cc4VV_f_p#WwjB516AtIB{8+P@fG4ps zmc6OZo)7b8qC?)8NI8o&6ucHPad;C)*G%geQEj4@!N^eM{x0#xm`8NJHuKAH*(jv2 z_Y2ej7xS}XyF<<>s$bJaH0PCmZ)PeF=e+IS?1X?-z9s{7r+?+-BoCD_OSRA35k<2# z$R->~`}5GU4tIYuGJlNMvSZ4!=;xA*Q#vBbsisUPdt=0nOO9(?HcPY5!Sr~4Kk^D0eX0-Z_+5yhuOG40tvW135K7dL9~+6~5Q z6!uURKsg#Zmw1ce@R8S9ndmg?oTy)b+C{qyUnM**pj4OvEfR_sqMm?ud=aTehn9Y~ zbnG7akdBaADg4x=L!`S~i6m052T^rG7s{Pop9w?4+i3G1Tg6AiNOKcg1TaqTv%D5_ z?(@T7ITbJW9|amFF(Xu}csxa5!y-)(@+_v+r)#`qCXzQ=q^rmfuw__tXD~-dTE5t{ z-;etgzaM7c{3|H8&RL+0JUho&Y1c>lb5vsQ8qRdLs_fK>b! zeSCcDjHoaLy+UD}?kZ4kZC-(G4`>d$ z!crgiXF%*2dj;MO7a7jehP};Z4YQHnD+5U!4oIe=5@e3|c#=TkC~+G$G{l(ljtB0; z9z?0Nsb*h4XSo7z@Bt(A-?-m+6|bYXY*UzxP?y!ify~f(kpZDj({GJkHm?hL4#maSi~b!I`W*-B z$A;q(+QhD=XFWD{L3($OKfzyaLIelw{)eCXni&kyGg~L3Hm+ zeobpp%Ec$Ex=(ZVh)6}8Y`se2h?KWUjIQBD_$94f2Jy5WgB^=XM#3laeA<55Bu-&; z3xR#bf^c9lDHH-KZ*g4pEKcX;@TqO1=gGwxu}Wpo!0AXsyX<6<`XTT|A|bcNr9P5$ z#cdG#sCpZUWVqKmU@eLT)ms2$Hz4^?20Ys%;tJ~*25tmw7fi)PtJm8H%c$d9sLqcY zW36AUcKGIxjyC7I+8rSfQdBTAXE!10_F=z1hed(1G0)QmW>O%m;sj5aURFtwN<@gb;V)bko z&p^a^r@@Ey(wbJps&8%TaO%#-34sUTd=_SjAXa?;ff`nU;x(Qj%v}Y_=Qpu z5*FzK1hXyh50lq$$M)y}3}0rEy}8Gs&qepB37WbBYX$^f)_pV#5*PcqW=?LQBenZwt$&Eij6*^ z0_6z5?@(s%iE2(?mzz-`#FO=+fY`UNJL{~KIX7Z^6WjQNqo{&A$h;@<2y!?UQ3rbR z6UeHH?g$eBy=rfb!?OzUe={nE98wAvD0Bt!ZP?W3vf_IXc%LofkObD~ z6Td%JrSXE!Jku+Ux1e)Y{4!4(?{P|r>KZvle$!uv)<)S22)iXY=qKLgX&oT!>gWBO zyT{b&il4%#UbdWN@eVITexc-^1+7_sb29!{{RVe@rcu%26Gt?NzVyz#?eKM|ljvew zM}1ib2E*zFD{K_am-zg$TX#T8}%t8Hs*OVt}jO;l`Qe6p*0JTNqkhC3;>* zi&|nr&j940@{3d7X9XAgaC@dvJqSBbr_|}I3uQ=zA{X-Upfj`u5Do-Dnl5AqkhDpY zQ*cql^=Xu2R*lg(l*f_L%X47L|wU_x0U#ywB3Ra(54GlO$1PsF!OXdUc$R*`5M zt-mH`>tS{7;}M5R*~>FziV_p)p(% z&rtGZPxYexjove<6u19ijz=S4x{(!g8%2bY(0j*w_%;=d?P$a!pK{aEcX~%RRMgB2 zdt#p3c|92^Zx*G zsrka~LM};&GGgxYQ8}r^zmo#3n`3qNFRf%R<83$Bl)%>*i~QD1bn?8H{Q_qV2m1Ne zo7Ct3Y#(WS?##Jiby?4-F+m5|bKxk-fDlGbg5^#$-5C~e;hQzNPmr*(rT8psQoswy zLTP+k1LU&}Z$B8zM6xkPg-^*an$o;%Db4=lXsjVqGZb+zVgjz-0T_Q|^?L~drzZLH zcF0hIEh7VO3F2%}2S}Jjivgv6BQAiab8bbNt^rgg@c;z+gSbl|A5ed~xVuDUGOl>qQa2)8*}d|q z@QZ|yG{wt@f0VX#{A@My9G7ENEYFav7`eG42p1|xCVS<<31=PV{^YDoq3VmwdI-u} z{C^jrd~vNw`7M0t;dcMZV~{$QqaIS#=I;ucdv0uhz&CnxbvcpQnM+H75zKSDyie`# z0PQSrM}NK?aD`3G2|ROi%|1cR7kl)WLXcieDm0}v&l)F!4pN#nn3J6_?se>9ajl7p|gvD?Vt zQ?S_|sq1_UAmKKYHo&cadW9=PiXHu=iTSEoL}NhMWrTVC%pEi-=k@sy7b#b)F$Hli zr19+9zuyHYM;u2%gnNBg{#Yd;%8- z^6z}9I||@!#RoR;Rj=s+K8cGdSE3;@O$RG7z+HR+gKF$F50qxfA2|Vm8ayzirLML? zG}7IcI%eOi`WNFtM9w`~vMOYU_eltkaBw+UzYMAr1!g3#RHJiSe=xLAoe6?-~=-?|V7COFT8i>84gEZ}{g2+PaN zEXAa1WRhF1BC<}uY2pc4EkmBc=?Hn9ju2J3bhd^)m_S-pHpYb1{eTHz)H*uH77V8n zcw{GX#-a&R24+Gn?$Fk6!PO`!35A}zZLa^bhzgmbt#3+s8i(|}^9onkm#iuP#mfOb zZrmOAUoANoa=RZGXQB0PnKSN?7CoD$$UB&MFLw`vyo+MXx!736*K?fLa4IOWSHW+Y z@dbxl+$3{a|NFiu6>zJal^j#wKqu}EgF4=)7|yLdkSW_pBfI-A7CwmIiprZfln|i& zj7@-E6Oj3?xz8e^R%9iVF zfe`GPTE`ADqD8l9Ic0OQvfKTFvtGmILRC2&u@aB5WxwAAPlhM0HZ$n@+`B1w5aCYQ zRP+&V1_2J}2LX?cN(BL1W?$1KzBNF1!zcm?hC~ zVzyC_RYR>`2w-7svp5t#2V0osk7c6-FX!oF|F3l%T;6wD-_&3<4@r~0DT=fL3 zG48BVHPAF3pZ$rereaijxE>RC3#K-^mL7SAW7YCmC%20#w@Z`g&(y3l+6?#_#kFpk zPmK#n%?WZN=Y$XBBrE5}lLy0R3!}UhU;PL1jF8Bv21$#xnLHNmBF}N1B$~ya1`(9T zhhL#)>6r=qWjtx)5Enk(Io5pHMs$AIH&AX*hsc#j=S^q_aH*TMCs|=FLl7W0-m`@cq%5F(=j@3b~Y{n&D|3rLp(@6^ywTFXn)0HV&<zWhMd{bimFPbc~b}uxMh(nX}3CF0caC8LO%d^MVC2e0qA}W^IqD-33Et z7&!g*b0g(~JCkJbE+dPnV1f|qOY zk4YS=uX~TqPoGrcg>m}Dh;=;(GbFuRwwg<*g;*7CaJ26>*4_DOWRlr{-K8TEd&u*) zWtuw;6W<*>4~tFA2_#67xRnGW=w2OT95`P1>=~oywMsd+3xznKx+pPJsVn;J_EC4R zp5hXmgcgcY_5o#R?Py!z^-t7;Yx|#k;f>PA{##QWgb#!j6*HX_`6RsqUwHHTHdLvF z1W-6HR2#fUFpHc5GlxoXvA5=GK`IhHeGUX|)PTIuLS^b-ewY8axpk4KQ^iklutPY)TK=7LD&u(|Cn+BVV2VP2J?84)j7;hM=S&1PwLhuBhk= zpsH?}JRAq;4(^!H&GbD23(8+f#cs2~(DR$>H(360d-i1ssQS9bPzLnbl{Ic>kTF>M} zQZM~@OpO5dZ`%JQT#@>pQo5i0O72>=D*q3>!_%`H6nbc^94b~Cw4!y+3QDxu=NHL< zOgPQ?7~;r?H$4Zc8ElrC-m&%EPxB%cgAY~rm^zU1&Il;IE4^#r+ms&uI!hP+(u=%) z0h(N8Injdx*_J5~#H(;<;9W4?aG*vG77g~WaE(6gBRlCCB2&OBj86$L$5~s-+GR^Y z875~4k&jXsWzc{$<2mf3*&-7Ivtq7Wif-c|mi0vNO9Jgp+fZ;QCI!CP@H{54ykwlO z7TJ}bf4%b-=@L=E1^{;hrt41Y3uO8+NGxhjc;h z#}A;Zs;KK>S$|q2K#kNv)+9}5LD!o>)r!s&8Z39j&x|**orO4|5efI*m?)dMb4?{j z0ap%V#KJm-s8C_~GBG^*PCx_*0qrT80fb#IRWVxDE9tH!4>&9p&kyVxN{8S>um@On zTynE*J5X6l*S82$_Yn|3ZWQCiVJ>L~4sQK6=bfq276D>*V-h2L-@^jD1rS2FajOb% zd)TOxX3evRr{e<+24PGK7YpnTZCY_>P}2Yz@K?vU2UXEetZdE#nI^<^QU5oPQaPXm zqhN=8-M5Ss8q6#SmIQz%O(TjWxgzXY6r9zPHDE{U!y?RH+&J?w$2*HMCq?T1C32Ma*^!{ViVV2k%04m7k5L^jwZFv-|E}xuAL6e|5 zGkTL@dQE;qp!`2jVy!cDt*Tyh9&~WG2*7eXc;M=u0W83%l#!>Ka)~Z zpu>=Pv@`fTUt!2Yaf8XwzNkxrH48}<4yaFN$(_ANdU3N36zzE=0L&vyXeM+$3v+hT zIcYL12Um>1{2hnU8ou@0;MOC~1sy54>!UUHzD0h+pKlb1+`^-cm==^deSKDf)AMeYL0+$^Cj}(%|ZKdz5EqR@BlDQPHk(evQvkE1v zkjc8^KI!iHYmsf#iPggidr3K{gp?m!(NPU~`NC%?znYJcxi*{9v zsiU8XfWY%u3tzL4W5=cIBPNR)#fmc{|`|x284GjT?Slk?y z6C~b6siZSQFC4*fX}KkyJ{!yuMpBNK^0caj(^+!CIr6-5YAt5jwP}kMQtPBari9B= zl3rrPKMD>;^YpxxZMv={hQaRyzRKj0f_|dzo(rg*E}(?h7X@RH4zse`piQD{ zU1WMUjD|$WOY9lH@mhQg@Z!uy)jesluo#kEz3~6pvJ6nyvx>dy3wq}hMlD8+7B7*n ze*|}5V581)ce`CLevL$m3r1^&n4f$zio+jr2OKJfiaMBD0G@jCff*DE4(t_3RRR0& z30l`#5?BKqaz8Wv2^Lj&FO}~7GKzy;fzqT36#R~{N9|(e-FgUIwCX(~%-w0&;%=rh z_|2qX;}-$ns%lFkDk{xcC9LfH60jABqWp^$=PaF;2o^^%kcoF7*CqsV#$a4O-9$`Xgz7_ z=qTLV4Tn}gsAe0X5{Z#kvnDWWz`1KQ)#qjr$F3Er?$3=)cCbk~GhkGZ&r6uyr+rbj zf?Sz7R6mHseVd=e3LQ)mr}x5JI^y^yhh11+bl;QK6~6MT@=@|)h25r1CQLf35&L2;ft2PV`O34GdC0RzgMSwj z!|C?9ZlEI%PybU4uGbP!I*X!#bZ!k43GW{?g4%%0zgfpXk0}$jPDmf7Htw;6W?2e_ zJv^~+6`52S7dFqMVsfNub2;HNNvexZ1eW_1i@NkJr#kzAJ&sAPx=fI-s*`cf*kH-xJ7-71D8mPD&j-!GWQh;Wgs}WJqNpVL(6`r4aQVJI!gBi9 z2?_~4skR61Q4PYCb$5u28(sSViAhghjlmn5@}G=4)e7PNTNhwCZ&EMZe_UQ{g*`dp z@3oBCZ?R%@38X^fp|Ni;VwxVpn=ktviTpmwx3gLC+R|&qjzpvH@7>6e23<=k7vebx z6(YVtOvbN=E*uQ*p`La1<>S8?c~-6!&<-jw{8B687}1fa<%EB zMAw~(<*L#n64QRV-5`mS&-sKe^J2O|&C%3E%mGIlW*~Bt8AADpDQ{!YMbmyzlpC_t zB@mMyB`z&55U(VgcmVVDut~=X@0zC{qYZhwxk}O*h1)%m8NDgzld<7~Cl)Z?!UlA5 zrKBA*%L!gGG*~JbGf5M*&3fCY;RZQ>5ub*^23Rrk%SRpO{GAbAj2bSSfBR~)7xRzB z_y>|3CXX*cq?E2~dm%w_(f7;$m>D$?tWi%UUcG$>VkyVmsO^W(0p(Q@Nc~LM$4OVT z2)qatXqk9m^()8;*bG&RJX+n!5)jYv+0U68cp~tQd}|P^CvC43!;~^Z)kw|OBpKst zA5iG2`l~&}-a{JaU5sc;j8K+-#Xm3;Im#I|A~Rm_UiI|Bny8yOM+*gf79t2Dw@K71 z$Sk*eol6Dh60l9G5T`^HPFL{jP>WsFtgg03O-g-|EF+9_br`qRBFWv5|B+_fjOv59 z=Jx&%qDwh`uXEVU#jZ)n*@QxvLQ_oTHz#(F{_B$dY;d4>XeYV=u)got{mD@z%5W+k z+0}`e6)W%(*ND-Wm2;tGu9d2{_#!z1hC{C$#p=xE$L{#DSOPlq*$zXEX!tivzgr?x znCWKjCeXbR*iGm`F5B74yWVzYgT*OjIlD$D{fnS*WajJM)vNAr`UjCt{m(lm1$2>M zYM>(AL9vdkcToC7@Nk0OG4WV_udQB4%_+u&v+9g)(T=h{#)6zt6>4$$vrS~HuXitk z>_zGL`BB|Xdzb_&j{61CR^1uCq|hLKiI89{^42oL8f60tS!o-Ni^M+<3&%(*i_fzvt#bi8oD&C3zk^2$3o zsP&$9=E(1K`aHD|bzi@~HhN2xu%1WiJ|JcH3Ugoa0OjSgs&{q2{nQC=QUj?}BlI+G z$clg(y&2$?jhe?l82;bA{&RKnHUTM>$d@7$rGL7sEcbyS?#I|(xd3_74EP2wLdNH9}a^rP7;lulk~|5K zwIwPAoY!h-t)t92QEP&2y))&bqi7Evn(wh8TVej%%1xFsYYbahGZXn&1BpPcvOwC87JvgO_A^)1W^%LStEoQk zL@PmFmWQh}Pg2MXFw==xtZTbte^BPP9+Y~94VH|E_({nYO_`2>*Ed>4Am?RVVsQhm zDYYg^0qxb9b-KJ=bO-H+wio3+pN%;1gMwJ2AJ3EK~e@O zy3aTtpy*xY>D=@kEd9w*QVl2K*?E`>iNaF=i8jk7&b+QHDa}34RAB znxj$xegz_W17#7YV8g->3aYemF*n99^bjY(jc0?l*8_wWAzOhJ|F#9gvERMQTb>dh~*L|!Wq{M?**Qf>JwlA9mMQ7KUn6^K_9C_-47!B~Jb!3FbaZqKN> zHMlV+iV-`S`MA9Q{$xc(NV;7$3sE9}A^PzZgyQ?wk@S&-!?B<-w!fWX1tQu?F5UG_ zu@HnX^_JFbw{dzy?-H-+p&%NpZo;p?2;n9NI*hmQCcV>grWsDPS4yrPSNaLu`*hco z(fTTo;dW=q98IxVU?-yvsGc{7xds*>;wa*9Y+}($JpvPUI@hF&xCD87j_XIyYN}f4 z4+{iq#roLBZ2yE_)Yu6;6fS?tJJGc@#z6uR+;|8ERn8R!{;#5S#YWo+P11 zRCN)BHpBxf-tSvDs|ncmg0GlyGii#vg-B7bFOHt4dYfm_37EF;#He;mT0Az#?x}tk zbS>&V*#M^i5Rz52oTt#(wq@IllJ53*X)|OQMh<@ScB*|c7uI;6@f{J&&Noa!`TmMF zAtD^$keN*c7vL}AOJsw21=OG@yb0-#%i~|Hy!n`nf5_Jx^uq-z6xy@j{J`K@I)S|d z290JqNUEz19=kjVRB_2v0$I@y0&&EVQOT&cR5a<>}d}k^^l!MIY(feTT4`+cS2)cPl$H^*o|~EO&2G#M(dHLAaji4(#sFVwd}{ zai&wMPBP)qlq2)!5*A%uA~ZfV9s=mR@zA1fzZ#h?Ft%UR-Yv8@jX?Nqjy3KL2BW`4 z)#RYIwfHRK?}#M@cL?p;MfG0YE`8-Lm0v76)7LMl>sQMp5%t0?uO2ZIn&ViS%a920 zg;pl`{wpI^vw1`BZy>46kIiN>vMt2X7SwaKGDE*yqi(0Jg%@2COIc(ZW|~4b$H2m} zl{2FYwc`9V0y~A`Ef;kc-kl+C-Le_RLF_Hy6_BAP@{tQ!F3R5}6Sa!a)FYHf4)o3( zHJ<5SQ$@WDxsGt0_5Gtqookm#qRK-wi+7qAh15s#UJ)C5s^B$lOah4%Ng3M|^1~|& z(~|pz)EWzl&0Bw6Um0j8WzA@g339|){;v;;?mdjVxy`lexztu_^i_AaHeX>5TovY1 zuAjs^dq2(KT*&h|x*Kc6sP8r5XB}CxGI`yOc};JAT;LP_P+ElG$HO}kgvb;18@Xb7!)0cQPX_j0{87Sn8d zgUKc}7B6VUvtZiuNj-X+AigFZII&Oc;i*Jt78c2Nov593!>-(3_MdxxzvkVCXl8H^ zw_M`$)}zg~3&%kdJeM-CDv4?bo}SXxPJCA4<|QNew^ps9)i%xMU8Qb9+7ZdN2`MMP zQ;7*9Ee$6z#lXyCQa^PF63;F>E?XA*094x7t{*QS`BB&_++)az#0k2Phf!Ob^bQ$H z`BDXKi_Aj-(Oq{zy+PD>sE)WN%=phRp3x`N@p;D|&m%N6W=7SG3GR`?@tddNuTVeO zUIM^hVR7Tsyoh&_tMsF{Y~^`>apN}%NQc<$Lqj|RiNQeRiPPQftiwB&0WtgFJM);D zBiN{RE&pR7GW9g{R-tLv#_@!65tqpeOy-z#kRe{D4O1Y%@^#05_yv5m9;QfdxjpF& zs194Ap0-PkA#oz*lJfAU46>{RzT0A>)(+uVt&3{O28q4BJ_QCTJ`)%>OaHgoJvLL0xuEbFHz-O>9HY#4vuz6@WXPp%^>(nI{zg87Qn0!;jPF*m@55SlDt) zUkM&TP8YFTMP;CtW;z$Bcx162w8{jKh+;iv-TF5OvFnW8m@$xBH09fmiQD0}##zi| zLF^{NtDno0uVppwGkLD?5mq(ejw?|!1OEu=^gH)3ezk~MElfY=Ss_J4BjsmHzkzo( zu1$`56Xbx99LBR>q@c$XF17JuaJ^u1KX_}17T%5I!vilXsY1Y+ixE{xTVM- zQyBNdwnFh>^rMrN!T$3AuwjO%J09U-P)#d<7pT;~Fmm#nl6P|;OdR!1EYD<)eEA$q z1{(yLd!1O*$e{(weF2CENF^4*xdr*#9}P)(_^vWWJp0aDOQlfrlP&eYFV63Oqa#)P zk*15I6G$Mvyg~RjI;9G2Pp1r*xqgOZELn_*iN#*6_mu>Ux9g85xcwoQ1z~g1f=BAN zJ9odLb6=DxAZTzNa&3_*0l#&ss7FBk6l1hU`++nqlrQ3LBmZGNhV6aYWv>)C^==gH zJWx+3r3M0+BDe2KTI@b_wkx!k3biZl-pmw{Z5`*Knl;)08HFv2DCbDigcTEi`qc8K zOvLb10lYOs%0FH4p0ldPWkqM`4RDaXr%@1s4&NK;q?*?4cB(zqwN%#UznTAlYT4tI z@R58un@M`@%G7~X>+krv&7E}mW?#r;+^?PWcSrM~T@PNBn<<-*hCK~JRjrI!5F5v6 zvX4$9d(v+Md3aU+tZr^KOixAn@2T;zJ!YS(`k4Xz^vfCIGrK#khSgZ|lMwDg51Ltg zOa8p9^AMOdHfcX47|p%c%-Ts?xu2)?{07u8yU=02LV-}p^w%cmLTOxId~fjihUgMQ zg!U0yU6g;;^#h{$^E1_;zF>`1E2i}NYO?zQ!U#Sacw@TgVJq^oV`gkTTQ49ZJMjZT zS%J2pH!?Rb!y2TCP-|Q_?+ZO`=YB%@9TM1ae%03*3(2M){sP9o-z%GV+r6LuK&2D4cJ&*c9BYYx@6Q7BfEmo+BDD79D=%^e{Xzj^MP4T1SWM6TRn&H|X(v9OGu(22jH`Y2X;u>cCdOlW2x9;@#0du=5ySROS_{GW$irj z*BNo=94CA0FLYTH5K@_fu5;G_zw96=_L;>-Bb`|#25(Q*T!byE@Qa3(;b!JkRj;wM zd;sy~?3gvs3t}J0X7?e<`oq4vvUdlwqYbpfQ&;+WkTvAuy}Y*hDZlwT>j>ZQ)lXZ> zXkS&xD+m`E5)UgUCYLkjnVJ8=es3kMAa`ZftJkLJ)yx5`8X?i_!o1@-zlEZjqr}vl4Y3V*ZPKn&P6?8t8MGTjB$|CE=trPD z+o(v$Z}P_}MB8%fw`Ccngd*Z>?~Tj%C<8#sdHfISE~UawyFYBQY~>w9^|#18#D`ib zJ2LzV+aZ^2C-Gz-{N8(|M?&5-JGN0%scqsldK9s1ao|^5ajw)k>Buo1{ZjKzDw9k> zw*1u~3mKJs@UI(75&s+G_lWw+{9Er#U4U(W0Z(1t#*4p{JXN?X5HcCNMLT`Jiz3wJ zbMnb!R?&A_?@DDy&KvvYC|vestjefT(fjeg&8kuah&lP1vr9#1X5Mo0Bb+LU1Sd)<4fXETAin$4rIvvODjHX^+d2eva56qNKtdS&j z=eEsBw7)rMDp@Q4A6%aWDMRF9z~UXGW^`T*Q?YtP2wfY*|nQi^5Ot>45t zCgl^%5%Ah5q(H4;c|wQ}sl(^#^q=vYYz!rj`cYN)GukVpU{kI2NVrqdm+AzW?Yf87 zI_De^T{CELjcPHux3JO_+De|Gd9febpC_C7dkzGgF`8gGn#RU2&aI7CHlQ1th&H~O zcb)3dfJY&3Gp5AH%J@vE84t9d9)O58D%jMs*x}0JIQAvq1D|dOo8DS7^gJZY^Y}W z&;yECU#J@SEGCsiuYl)>M)iT+P`xl=tx=ghi({3n|=7gBC*z<4}7tAiiS{VHzRSWl($eu|qwS6)n1TElj!IAj4*36ioG%r_lN`a(2 zo^PpJ7((+mCx7T|9@L7fRAC^M=Ms>JEK&8qI$IYk*z(@wqX#OI#57RjhZJt0#r>S3 zlRoJ>if*dp=s1-4x(6kJZLcoZiQ^#zoKUv%M%gM@kLNC_BowXU|Buc|aq}6J4R^;N-;bDLU14%Z2?-%~ zb%edW$%&BCCrqV3c&Yo0ggdUz8F3H2E5SsR6%9S>lJ<|nx~eN#VsQwVm1})FGu; z*|q`b(iJA;ic&=DgVB02erZvj(HDNUc1OrY{4kl=JI@AGOXaONj&Fx6_w0*CvKdyo zFe=~O9JuCXMP_1!j(Gm=lc^5Jo2?jS32$6-8`Os~_LPZrGMJlO+juIAk15dYpDOZI ztFUk+9Vv{^f~(HRj{vtjv-o6Q@pL9%gHVw2XLL84+BZE5gK6vuhpKesXC zu_@{$>fnpeFA2V%pM7O_g~YmAteV-i4-?m2dgBjV$R61h0|u?^C=qC8zt z<=D=@iVgsV*`$9~1cP%~T^hBEhw8T!zbn3)%sUq5C`ye?7v9N$5zgQ06N!R-Re%}Q zsf6wXeOnb~f<@LlMD;7s8IAp^BvzhxF|E_=iqSaQ$W4D}c{_Qr^N%qRi2jET`vydS z?*0`PK&?Z!&EY>jY$s+Y@GD@~krjf^Emc53eq#5B)s8Mw-Git5#wNl(eC+*&Dya@lpbrc=hZt)` zM;C2k{=^fKL*WX*Ga5M#HtM!;2*y_lRTp8KP^PQTrH9}eL^5X5ZgQfA6Y#g;6r%X9 zU_!&`B=Tu1*{*|csi?+@-zeKxLbeD*Ck0#O)=9}NGan5`w3Ssj6-GrpB{_J|nm@En zxV*TH8H|mO-*n`{(BBFYRR=A44Nbn8=2!g5n={|GAL0m3La^pseNIhd2&Op8^7R%; zVVX!Gz{fLjdu{n4(Sco+GaX|jpp-_b8JldI^+GFAJO!R@{mqE@Y$63n5H?U^=yTCs z_(RX zH6)Ke$?u(UHK>-c7*e45|3ox`SuVQ9bz@vTN_f5ADh+4meBe4S z5zNQPx_P5whxl+=mnfSE71Ce5eX}NMcVmBlPR_X^|K8|Lj`IL9|5L#W@ax*OPgtP`KM|ORL}B! z4>ZE7S8>RPw?mxCnNHNDwn`^Xg=rTCuYtY&paT>0$gxI}8p9fE(3nQ;`Wb2qFGb;0 z(SvY+&H#b5;`w6eYiyL_bynZiu?6rLyBn#hrGS!fq1J%iq#Y?;Ow;7J&ET|co|1}^ z7uS;c$@HIi<;1!&V!^H~mysyP*G*zfTDJ&=p_K1rMf7IkYneUyka2^uU#7-l*}8qj zJIjs#@1=2JjY{Gj0}ljperf>kz_wKQHw#ujFG{hT3q9Wzvc*Bx1FSw!;`EvD8DoHb z$f#!1ib|l9x{xd*vhqZDo6&1n{%q*0r*7uG!pvJ6Y#yet_nHl^4$ z=WN`bpJFf})tR?`z&9aWt3)%@K%U9_zpI5r2dCe&0Owiqh)zP5cGdL! zsd5g9HKWz5JsWc7kz!fYrdj3#^|F&3bpg5QM{X; zP^dqIE;~VQzPScuSlT`UYQiR%384t&F9LPaLL&1A@757@0)VOCzmGCcrNju`?+reO zdQ}}Oy7|CRI^VbJJ}#M^EDNg7P^JxJ4{rc$Ji2a2_z4LTTVE*(#6SU*@XdXIs=$$dFGmmtB`J$SOSb5luJ`!HFFcmLG*Jwdr2T+w-x z-Xi_oAiz-kAU(mBHa|4o5&YHk)goCRjqW>tyM0(tHike`c@8-qcgdus^zVZ$FeTYSz z0QX+I$~+VZ3I(9Pqau!01L~xWWfbvyjobPb1oSl^KRxpUvnAHL`XiF8hGNe}rdKJ31)xQ+h z@O7OmlKhKuoU~#*1r6f~}~AdK%yHT-94F!%1k=c4rWq>qp#r%K{gg23vr1Ue+5 zOy9zvH@8j)((;CpG?8n+elE*BfP9#EuTRDMkpc2~qkv=N_FoAC9uEo_7byMN2UZ+1 zC=aKrcrCeLZFiN@vXJ>Y^EckD`uCM=JCG_9T5hgjTt$B#UN}<%WRF?QXU-`6ZzAr3^9q z$Og>@`75mwNN2h1=}1V6&AxzJU5K)so|YzzU`A`{IHZWQ!$eoQDI1~GysW+nMySKN zc0ourAlMqXc3a}X>e{FN50GPTSrQVbjIEn#uEpj0CP-VBbYxTb`4)!a`w3$&k5lAa z>tW@9)^HsX7s?L=j8||=q7!;4yVkDgJb+^EG0w}gz57)D#jv)a$7q%yJ_y4R)YVjD zjSv@&XIj%gYJvDJpl&Im_TO?^_Qb(d7Ofr5Y=zH%K?1=p{7aKMyIzvh#VsGj3sP@O z>UnwUU4tBulOHRtd#N;vv)nJ`$O~v1UXRVf?vJ|ZD?&z1!0SBP5Oa%$ly3xCnyQb@C|fcjzeS;XK5%< z0*R#juN6IaDdTnUH%_i*cPy6#-ldYugL>jthR6a{4YR}x)=3cOBdpl{>1{4Wt1^{> zGQ*>&SINYC%X)r{3eDH3{|dy9P|(KuR(!D8T``ihrNz}p>vW5%F1jqTW~Xs7Oo}N( zGDT8Xb^NDtKMyoVw5gPhT2Kx%t}2yX05oN3zZlah;kcc{+44Vj0ICvSb2f5aBu|;L zq$=xD04g^GstH5M;olea>VQvMrRUgWm{5)#UTfr?1qkZQ>4n=;wuq|-j0ff4p`*_oF zJ_ZxzNl7~&k_5^{Caq4W0&DCudV6Lo#aFaR#Wh>RZWgMdZedc=ERP=+Q%`}(yP4C7 zwT~5xWK~#(%0Dei3l$!vtvR$qnUnyL7WcJk%`>Q-{ITnUQ=jAI505BDYy&gH&bR$= zN>nj#pm!BU#;jfpw@oDccB6Ry`>TXQ%2HR{AV(UjQ~X+Ab~UttbCSP*2orMpwS!k! zlF;ln;G77YQB@&4xqS$jzZLuQ1q#A-_MfdcEWM;43C|<5Dh%Lk{MQpoJs~^8EJS!S zn?iTHV!$)-jrL<<6DKA}RakY3K0!jB=xc8vQSu4cdbpP~@1IU}ENhJjW;t)Z^Z?J6 z$gXL-XtH^_j2|^8`gS`=!7H~-L=`n(J!D3 zb4|6$U0m_E+zzaIlZND>pFvAji%2@Nly(^XUKiB z6@Kh|1-2Vt`W1{nVWUp*YqH)NA~O01787=BNJ(C1O)!`&CRIVw_|#!*&Pd>lrg;2A zfIflpP7}GQ<(qnM0{~h22uOZHfVka}6l^~o;}vu$E=5XCF*B`j@tOYg{Ojx`Y4Pv80;Gf@?-1Et zqEF_G_7auwejOfN${u@8{V^z?=mPyQR!jxL2yQABUhxe=% zjJxLD*Wuw3DB({#_pbd%>Gke-C|v`xdUqcKOu_c%&Qv=^j7Ru@`4KhVaH>hUq<#L> zyLR9Go~Zy4b1H`|GXF@ksozGx%`Q%+{Si41;ZvSjQooZJgNYK|Di#t0#i0(VHySAe z1XBBpJY>@Ui2>S+96N_x2fcOpQW>dUi~ z?U>U$G5Cba`HfWkmAJoOcGTT2_ud@ZT3F36gs)Rs+c9i91CgD}$DNO=1v^KP1p35b z5DM8QK8qKae?@p$9(MGceq4$M^CdIH3kBC&+H0efBUQw20iScOx12!2_B!Rev6^wWTcyBff_aOSsoV48~~J zJM^{VRJ;)^2BX5g#7Ro2o(wU?H&#UpW}LyYDe!|s6c7wDtX7?Z&GYILI2mGj{UW{E zUN)x@uaErHRSbUreToV}2aDi4HNqnC4s2iVVeGDr}m=FBESOP$(okf1BFocG6t`6=A6GCc}pqPYpc?Z#{lnNu5^LZ;9__^z*#XtDi z!j>Xq&TFKvkhfb{loe zDt%A7TM*cc(c88x7?L;dL8Rx->1tc$AdQz>^8kX78g*6>6F%~CYhJz-rimC2_?@ah z4;0<=UeD4~&n0-X4ie(Ggpr-Fnvn-^>r-+V&|mQS;NpMg@?tXHWmJbCqrI2X3PY=% z3I-b@d$2vi2C219I3(>TIt>;KH!E@swJIXY>UfOZ#q<^{rMo+pON%bk+%((UXqhq? zMJ5a{TMeP&H?BzPhU9Fd_LFY?r6nF`JwA*9-piFX)!Fefrag}be3y_Y&VaYg5{zB! z`+&$>LkWFNGT32ds1t#40)^oOqkV)3s5|+1lgF>@$Ta94t}a>cJ!H_PLCUd_dJ_?M zxO|6Jy~RxnV#aO*@pY^}t6iF+{aHPJ)Co26KuQAqs!$`F<^cn+MpOlT9)stQmE98> z9XONjv3Gd4iDxALOqNL-g)i6MMNc#GeX{U1O#F#3B&OjP&qO(3wg$Z)cq-XLLQPQb z%)f$qnj*=Ky^ZR>WK6$k>oUed)c`2T3BgX2d95+$Rz)nNMo#(v zIs=gV5&N&|0wzI%%%%K1=p(8u6<>;0cItlKUxq{sAGRIu@-esJ79$!M8oycNLh<D3EMHWhb zt^i?EUOfU4USm*}nH8d3rU?h1!^_AA!7`?6r7?nb#wGf;ePQuzuRn98`jitM7%ODS z#sFaAlF4l!!FHFrX0PlVU?C%UzWsgi@vsV1&j8Y2^zc|{(X4Xc!xqopMFYfCAnO%u z2%iQMNpazF-ViE}s&CIEwvLxgQkd(8=cPyDxwS6})DDHz*2(WQV~r{iBULyq3^OQ& z9aZ3Pp_5wPal@P4r;yIANq~}w5~Ul)By@-^a2Xs`A}y_cM!|2Gi?{0ZQ)XA!tIx1u znhA||T88R^G?T%t5xdG%8ob?z2TacFsO>*U={+V=Q~q6R#Wf(;KVDBMMT2M7_>)mC zu9eQL7J1=}^^$j2r(I~h_G^hce`bCXN8RGY^a zYXkfpVM^!$C{Cy{t(apv5;nzE)(1WEY8Al!KT{sF;bavPW?w)lu#8p8vLavC>mTK0 zzd>r6^+(Y%I9JGUqCHHZJ&Lj4SMjsN2CN~%1LFkR3<%NeR2NQ3U)6NMJxThG#40_< zE%uZ5?)RUaLQ~v3SDB$qLWDZYdy705C_CEIjyUP9U9(8K|LJgI$OpfWtlyRK@m@0E z)H9K{%V6%=igkcyr^V6GjpkQSo6s1eA+OJ64G)lbyueA+eEj=C*fy9u754{!jmyH1 zeJ9uB9GPm{m8aTHhv~?UmyWbatJ>Kj!=jWj6n6w&fl)3g*uT|!Ce7z^y$NDjW5 zlR->*nGO&QR9e`3n_B8wZlzE)e-&(pkSp;-T_Erl^BO0>5!0tyVe)Rm#YAA`dA1Eb zK`;}wi&Kza*uwJw>sc#yDwdQ(fb9k16Pwyo5>*#{UKXE%fSnDWg*q9vGt=-;P;U}5 zD}^ebl~J!;z?e(xnGV$Zy}bz$8{XgwvhC~rtsy86pZ=@72M5141xGFOiIN7*CThZ4 z3f?BOEC&=9fVmxBdmgJNQbcxa27YZd#zEq_i8Ixq%`Ve65JKI~%QMduQ}$ltSFRm+ zPCIzjK^E`_3)b@_#OtbV5H-i0c{3mN87Qt|jEZJQ+0QL*X~pG|G_~am|9L^_mluHi z__XcbJ2KyXzX|RIJ?z*5m1*{UxpmRwsZ&@N2ko24S3=3Dn!~5dF_jAV2ZtX`gbVb< zi1uN!*D=vHb#|0>UJTI~B3anykjbs03w~8l+i8{8{z~)FAEb{2sI^${s*0mlI;59S z-JpZ0S-!aRMTufb_mW9tYW)D4m~|vNB6h$D`ADS(+Q;qD!_ha;@gRH2=ugFBUhq61>ol|3a8)qLLb|-*vN_g?kdZSIlfxiveVJd1c7_O6V>;IEfx#v zx7PchAAvV2RzX)~I-@=d!cWe%7tWJyDe9T1;NJz4+*c>|^AckjCn9%dJmpV=&eTn| zCICs^$7tK37|IVJ1$*LN$97!6GVdkVav?V{g_OqUkA(SGEg7Y^oYI}x5gai4708|t z|G%x#4=H|8WCq8BK3*+!7^01L&?>D+Z+jzC^~iSlLzC0C%5c# zXu7(97+x;BmdIjHllh2l!A#e6qC>u0Y_0*Z&dmje$mXpa)xSH%joG4ts9d|(Yit-0 z!LeE)0NsOYZGCw42tTl{`GH$)y$JmNcLw>l(&aL2`J>{RGCt^SLtlEDh#2JIN$xTS z3*D|BO9L!*E@v2!Ox~)jL#i)>130HQmDUXSxUL&?Ij_1M{>HX=*BMPy4!2=D++INK z^rHjKW;EH#Sa4e>7z!^Mrz$1RU=7afj9J3<^)tVY1*R=EfcvwtihQfRmVYGl&@HqG zQJ!PNZuwsIb)-3(Guv!@629{0^vX&F($ZWOiD$wcmS*j~h`x}`?x_v>BiNEfgN%pj z_OsK8fDJ~oJoF6&Gzh^@%kQcIP}cv$p^Igi6TCvIN2%;NT^;}2m-9rSzuo`PnGqxH zdx>fuPZ$T-JF>ktrx+5A3Kl5wM*l5ja1SIZr-m!u)^Zie`#HYUsp)`f{f=X;ti3(2 zKC8{~T5$IOSa&b-lO9GvfwaFX51_}p?C%j~9S8nz$KLC}f&7v$sR9Bw$9@V@VzE1L z*ocl7$d$O>l?Vn(Q}uYcVY^V&l-j4VE#u_oxEdOd*@l**h>h%Yxv8xJ*8b|Tp=1yl zg?M!@t!`BuVp$nKo{7$*li4)mvu!Q%3V7K>BQGEl+impY`*MmGD&uP~t`rW(s2 zsF?6v5kkA_5fT=WmqP!RmL@p|J4BN#{t{A4PkXEnf%uX;E#Z(2HK-bSeJSw6G5kt{ zLBUO0xZCQ*6fOb{SA;F?7bZGI8pze zMKv98l2L9uXx`;;z^x{=t+!_Z_eTfR?|}_z>K=ANF!L4_+i>0G0D2pC)?hn5flt%R zn@hQGwi3v_y%GboF^CrFmy%`!JMiI4B5m_T`CLgp5>*c0TBQz~6%TL;sQNC>``Rc8 znYPZTRE;GM?khk3l{shEh+Rv85w+4$X9ya~G!}&z?bvAo+y`mbYl~PN)yrG}4-M*} z1qF!bCr%l*OEvbkN|o{AVpbP~T2>LugyThBSPxZM;>AN3Wq;vknoBTQJk(q^l8U9P z*kgG?&U554+<{tsHdMIcC-Aou`lh1OSjlRJ3_0vZ9Wq) zA*m(9FcHMjHg#UBpSUX>*L!7T0aI~X8#tj-uhZZgo-BYQ4HB)lGI}fXx*uqcd>dOI z=|M@%4jZXxakk!8&|E#f7M&RDiM>?;{Necdr%zHDw~)a40fA`K{a$xV@X~rqdXANR z^zOkq9Mc&H!8g3Zz}cFztwMzgWI@L3My4splLo;>;}N+@vz1-PXUfl$7M!5bYh{F4!} zmjgB1;NgOOtW<^$nfc|dbSQ?afD|)3M`l%8D2(9tQrUBI=WcLFwNsDLkaQ_miyCE zpf4p#@nE07vFu*F`U^`DvJ+Sz0RAm#03Ue04F)-#Q}h0XXqg9|uuP;Kye!ag|%;GHB;DUb?z0e|tKmpxV` zA3@q`j1@rM+>^KRNr9go<-*%yYx;oS@TO8yHm$uPlvDsvytpi;D};`hYAy^AR+Bm)J1*Bp2DSZMKqlN-%7N_1T& zwI?I;O3(k^$fcYRB1r+!E+omlVM7JH8>HT~-ixSgK!kVH9||B!;ntF#8LEA;=`)q4;_+b$%aa zRWj>6S1b1a{QOQssRW0O7j_W^D1vFpi<@}|e>d6fC`vK6Bl8}CWNV?&0{Ufy9RpF{ zRl{ijEYeJcv4>;8dVP@kvKOOWcgsp=#* zFRUXb=^%)@F3JfJ%C*1nXSMB|tn|Zo>@l>8iv$%42D(jH>71f*Xi%9qOgO$tm{g&Z z8Vsax{O6fzJ_ej)>ed8oEUU*|AJ!o|&{(;E6uo~Zbbqw*CSJ)<>#9Pja~Z!j@N#P+ zI?ljSqofKjnk5i94OG%8xFeEIBMhjfTl>-YnlMc`V(lP`-*7u++^FSG7f)0NNS?~m zg$TO{qO7!fl|Ph3n_u;EcFU6Wiina9;+aF(+^N1Iml4Ndrq&t_1@|&cPNa4O`=-rH zTXZ^3=U8MIo!@eEXBBVvo_|*F!>qyxfqXjDoG|96OJ(uw1E|g5Ya_p-cL6%GQEo^K zraVvib`W#+JB@fno@v$hHtgn*!jVI_fNP*#Zn{8*LPfSNHp?(|BB^Dbk+V^Rjl~xIviZ2H(#djE5ynNqdgqEtUP|?*Gz6upB>IK07BAb}nXu%_>fAnhw z4(^@vvIpAgA6x}O@;`|vA&?h0Y&F-T%jqJ)x)-P-fUpVVmn4b|5Y;B}J|haLijC9I z;ODPoU{Cy)B%mewAw=Il67E1u+aq%ktWD2baT9ybmcBy`^%yBJ_!uYGMl*SWLNtLY z^ktmbbvJ;tPAB)`;<$Av;K3x(odJui5~Bhe@8EUVvm|u=6rpFSpdC4nyEy`xhzj7n zmv2GCEgjxq04|JjJhvH#wuF5l;0jbw39sdLEE$3)olGNtZ8pK(rbrdOC9iNbexxbF ziErmiDmN2G1>JWo4m+%g4WB~^p z>0s>%*fz@`v=4*VhX-^DdTWB*JLjU^I3T$l5Ia9{F1@%H>ps+m^ zM5P=)R)L+qg@G#sz=)_1mKZb}J04`RC2HCa4Q2UEU*rvap~m3$tkN)wu{HwykwW? zv5-rRID>7>a=VBntkJS#J1vahNiS-ka!I&2C)5HxAB$N>s3*;6wi4q6@#=eKDY%W2 zj(U}j@cr4qt}zrwA_M{3R42z8KL&F$`3VCP9gb|XeO=~asEifJ&j`N>=0@Px#N+M+5&wQN^YQ6S0Lwn57`bkm4aE>}owG4a==vGYo_1^&xLYX^>x3|6Q zh#1yNv+zT`JzyMAI94h{;Qs$e$@p0^oZg`Pb%6i^gjT8_+HPI&%u-~X7SbHK_YQ3gD$-Y&f=GT zj0#ggPv{|Zv6K`&{W&0ps0OR1zp<`eAzrp0v;sq)_4zFh2Dus)tikZ6gm>QjHr6+k zq-I=h(D9w@O+CW9Xny5X;Ru?+=k)^M-H_w)g;>YD$iRc)$rWHy#QO_{-nek{4(^m3 z4t!sRVJ|HL5DTUWJ(PT6e4oo1J6~;D%e)Dz@CFcqlB1%tHeY-7Gtx49SdVn#Fv)iW zr|ESaU_yyY5Cw*o0%E;afmnfincK_w!vIa8=P{e*L~T6Y^=1-nZLT%A(I{Y5rIx9* z$Z|^d)2D&8w9`mM8Dn)BaGG4mdKrgj&1A-301JK~#Go&gg8O!3euUP;Dno17!5aZt z@&jwlT8t$Wc*+9B+kq>o_RjVkWhRI=LszUut$j>0LhpJe;eb)WUk_UfF{K%p#<5qn z!HH*j#Lr;!2G_nu@2OgOVPpo$Sz{<_NB;}N?Fn3c{RSjf?;|nLG6|Flnhi8Q9IOZ6 z+LV>2Gqty(S-qEs;@<5OgVm!wx0>eb)3C5}C}L|16tA(cu@HCBxswFpsqKZ%k>D$! z;n(zUaYwk&0IP;ylKyUe?uJRAAND+t^OP7-Xo=#pd{B{q zy8y5$>mxGOAQRKI?rPFQ@Hw)M@Ou5!IM3R4>lPk~FLGhZS?=3k8ZZRhRsz+lXLKbM z2;)*+P1Lq0#5d^kWF`z_B93G}JGsQYL6lX{sdUZSQ84P-UhshImBEj%=v&Ks;PZK7hfbq~ui$=)H-VTDo-2cvzPUewFf zRa&{us(`YQT|R5OsYOV6i-c1J8&UEyopEDE3a(gg?5(*CI(SpFD>*SL4p-#K)I$2r zxZ^W4bY7?~Uinp*x(1j-!-0T5{_ZN6^S`={WW>@}3ZVTg2`zWQ|M5Ur2m7pEF)r(c z#!;6f1qthIKM8TGWO`iOZ0j5=^-L~()%RYE_^6&$Q%MfVf;i7sZfZ)N@T3VU&Z6M{ zW`;dzBFJQ{+?EPFXNpQWryi>Z<}s-n+LY^lk>tRFEPctF0NOk<1?_fM0odRe_Zena zGL1*YoulJ@FGwHv!H7}t0fHVL`pu2B$Vs9xkhPD27T_+0 zXE)!>+R&%*0QeKbZ<;kUo_?k;tg?bkl6So{=2#v0J?itpCg&3IUJEbS>x$^&-%^3( odcR{d${xdb1`$Kfd>nvE*~AINEvq!gjh%S}sC7)TfqIk2>Tz)%vH$=8 diff --git a/constantine/bls12_381/src/tests/g2_uncompressed_valid_test_vectors.dat b/constantine/bls12_381/src/tests/g2_uncompressed_valid_test_vectors.dat deleted file mode 100644 index 92e4bc528e8937a311b502e4940e5e363a1f7c57..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 192000 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�uc*gWwz@d=S2~bA~LrFNXzUL(>hGG#V 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%0DJH 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�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 { - let mut e = $projective::identity(); - - let mut v = vec![]; - { - let mut expected = $expected; - for _ in 0..1000 { - let e_affine = $affine::from(e); - let encoded = e_affine.$serialize(); - v.extend_from_slice(&encoded[..]); - - let mut decoded = encoded; - let len_of_encoding = decoded.len(); - (&mut decoded[..]).copy_from_slice(&expected[0..len_of_encoding]); - expected = &expected[len_of_encoding..]; - let decoded = $affine::$deserialize(&decoded).unwrap(); - assert_eq!(e_affine, decoded); - - e = &e + &$projective::generator(); - } - } - - assert_eq!(&v[..], $expected); - }; -} - -#[test] -fn g1_uncompressed_valid_test_vectors() { - let bytes: &'static [u8] = include_bytes!("g1_uncompressed_valid_test_vectors.dat"); - test_vectors!( - G1Projective, - G1Affine, - to_uncompressed, - from_uncompressed, - bytes - ); -} - -#[test] -fn g1_compressed_valid_test_vectors() { - let bytes: &'static [u8] = include_bytes!("g1_compressed_valid_test_vectors.dat"); - test_vectors!( - G1Projective, - G1Affine, - to_compressed, - from_compressed, - bytes - ); -} - -#[test] -fn g2_uncompressed_valid_test_vectors() { - let bytes: &'static [u8] = include_bytes!("g2_uncompressed_valid_test_vectors.dat"); - test_vectors!( - G2Projective, - G2Affine, - to_uncompressed, - from_uncompressed, - bytes - ); -} - -#[test] -fn g2_compressed_valid_test_vectors() { - let bytes: &'static [u8] = include_bytes!("g2_compressed_valid_test_vectors.dat"); - test_vectors!( - G2Projective, - G2Affine, - to_compressed, - from_compressed, - bytes - ); -} - -#[test] -#[cfg(all(feature = "alloc", feature = "pairing"))] -fn test_pairing_result_against_relic() { - /* - Sent to me from Diego Aranha (author of RELIC library): - 1250EBD871FC0A92 A7B2D83168D0D727 272D441BEFA15C50 3DD8E90CE98DB3E7 B6D194F60839C508 A84305AACA1789B6 - 089A1C5B46E5110B 86750EC6A5323488 68A84045483C92B7 AF5AF689452EAFAB F1A8943E50439F1D 59882A98EAA0170F - 1368BB445C7C2D20 9703F239689CE34C 0378A68E72A6B3B2 16DA0E22A5031B54 DDFF57309396B38C 881C4C849EC23E87 - 193502B86EDB8857 C273FA075A505129 37E0794E1E65A761 7C90D8BD66065B1F FFE51D7A579973B1 315021EC3C19934F - 01B2F522473D1713 91125BA84DC4007C FBF2F8DA752F7C74 185203FCCA589AC7 19C34DFFBBAAD843 1DAD1C1FB597AAA5 - 018107154F25A764 BD3C79937A45B845 46DA634B8F6BE14A 8061E55CCEBA478B 23F7DACAA35C8CA7 8BEAE9624045B4B6 - 19F26337D205FB46 9CD6BD15C3D5A04D C88784FBB3D0B2DB DEA54D43B2B73F2C BB12D58386A8703E 0F948226E47EE89D - 06FBA23EB7C5AF0D 9F80940CA771B6FF D5857BAAF222EB95 A7D2809D61BFE02E 1BFD1B68FF02F0B8 102AE1C2D5D5AB1A - 11B8B424CD48BF38 FCEF68083B0B0EC5 C81A93B330EE1A67 7D0D15FF7B984E89 78EF48881E32FAC9 1B93B47333E2BA57 - 03350F55A7AEFCD3 C31B4FCB6CE5771C C6A0E9786AB59733 20C806AD36082910 7BA810C5A09FFDD9 BE2291A0C25A99A2 - 04C581234D086A99 02249B64728FFD21 A189E87935A95405 1C7CDBA7B3872629 A4FAFC05066245CB 9108F0242D0FE3EF - 0F41E58663BF08CF 068672CBD01A7EC7 3BACA4D72CA93544 DEFF686BFD6DF543 D48EAA24AFE47E1E FDE449383B676631 - */ - - let a = G1Affine::generator(); - let b = G2Affine::generator(); - - use super::fp::Fp; - use super::fp12::Fp12; - use super::fp2::Fp2; - use super::fp6::Fp6; - - let res = pairing(&a, &b); - - let prep = G2Prepared::from(b); - - assert_eq!( - res, - multi_miller_loop(&[(&a, &prep)]).final_exponentiation() - ); - - assert_eq!( - res.0, - Fp12 { - c0: Fp6 { - c0: Fp2 { - c0: Fp::from_raw_unchecked([ - 0x1972_e433_a01f_85c5, - 0x97d3_2b76_fd77_2538, - 0xc8ce_546f_c96b_cdf9, - 0xcef6_3e73_66d4_0614, - 0xa611_3427_8184_3780, - 0x13f3_448a_3fc6_d825, - ]), - c1: Fp::from_raw_unchecked([ - 0xd263_31b0_2e9d_6995, - 0x9d68_a482_f779_7e7d, - 0x9c9b_2924_8d39_ea92, - 0xf480_1ca2_e131_07aa, - 0xa16c_0732_bdbc_b066, - 0x083c_a4af_ba36_0478, - ]) - }, - c1: Fp2 { - c0: Fp::from_raw_unchecked([ - 0x59e2_61db_0916_b641, - 0x2716_b6f4_b23e_960d, - 0xc8e5_5b10_a0bd_9c45, - 0x0bdb_0bd9_9c4d_eda8, - 0x8cf8_9ebf_57fd_aac5, - 0x12d6_b792_9e77_7a5e, - ]), - c1: Fp::from_raw_unchecked([ - 0x5fc8_5188_b0e1_5f35, - 0x34a0_6e3a_8f09_6365, - 0xdb31_26a6_e02a_d62c, - 0xfc6f_5aa9_7d9a_990b, - 0xa12f_55f5_eb89_c210, - 0x1723_703a_926f_8889, - ]) - }, - c2: Fp2 { - c0: Fp::from_raw_unchecked([ - 0x9358_8f29_7182_8778, - 0x43f6_5b86_11ab_7585, - 0x3183_aaf5_ec27_9fdf, - 0xfa73_d7e1_8ac9_9df6, - 0x64e1_76a6_a64c_99b0, - 0x179f_a78c_5838_8f1f, - ]), - c1: Fp::from_raw_unchecked([ - 0x672a_0a11_ca2a_ef12, - 0x0d11_b9b5_2aa3_f16b, - 0xa444_12d0_699d_056e, - 0xc01d_0177_221a_5ba5, - 0x66e0_cede_6c73_5529, - 0x05f5_a71e_9fdd_c339, - ]) - } - }, - c1: Fp6 { - c0: Fp2 { - c0: Fp::from_raw_unchecked([ - 0xd30a_88a1_b062_c679, - 0x5ac5_6a5d_35fc_8304, - 0xd0c8_34a6_a81f_290d, - 0xcd54_30c2_da37_07c7, - 0xf0c2_7ff7_8050_0af0, - 0x0924_5da6_e2d7_2eae, - ]), - c1: Fp::from_raw_unchecked([ - 0x9f2e_0676_791b_5156, - 0xe2d1_c823_4918_fe13, - 0x4c9e_459f_3c56_1bf4, - 0xa3e8_5e53_b9d3_e3c1, - 0x820a_121e_21a7_0020, - 0x15af_6183_41c5_9acc, - ]) - }, - c1: Fp2 { - c0: Fp::from_raw_unchecked([ - 0x7c95_658c_2499_3ab1, - 0x73eb_3872_1ca8_86b9, - 0x5256_d749_4774_34bc, - 0x8ba4_1902_ea50_4a8b, - 0x04a3_d3f8_0c86_ce6d, - 0x18a6_4a87_fb68_6eaa, - ]), - c1: Fp::from_raw_unchecked([ - 0xbb83_e71b_b920_cf26, - 0x2a52_77ac_92a7_3945, - 0xfc0e_e59f_94f0_46a0, - 0x7158_cdf3_7860_58f7, - 0x7cc1_061b_82f9_45f6, - 0x03f8_47aa_9fdb_e567, - ]) - }, - c2: Fp2 { - c0: Fp::from_raw_unchecked([ - 0x8078_dba5_6134_e657, - 0x1cd7_ec9a_4399_8a6e, - 0xb1aa_599a_1a99_3766, - 0xc9a0_f62f_0842_ee44, - 0x8e15_9be3_b605_dffa, - 0x0c86_ba0d_4af1_3fc2, - ]), - c1: Fp::from_raw_unchecked([ - 0xe80f_f2a0_6a52_ffb1, - 0x7694_ca48_721a_906c, - 0x7583_183e_03b0_8514, - 0xf567_afdd_40ce_e4e2, - 0x9a6d_96d2_e526_a5fc, - 0x197e_9f49_861f_2242, - ]) - } - } - } - ); -} diff --git a/constantine/bls12_381/src/util.rs b/constantine/bls12_381/src/util.rs deleted file mode 100644 index bd25dd06a..000000000 --- a/constantine/bls12_381/src/util.rs +++ /dev/null @@ -1,174 +0,0 @@ -/// Compute a + b + carry, returning the result and the new carry over. -#[inline(always)] -pub const fn adc(a: u64, b: u64, carry: u64) -> (u64, u64) { - let ret = (a as u128) + (b as u128) + (carry as u128); - (ret as u64, (ret >> 64) as u64) -} - -/// Compute a - (b + borrow), returning the result and the new borrow. -#[inline(always)] -pub const fn sbb(a: u64, b: u64, borrow: u64) -> (u64, u64) { - let ret = (a as u128).wrapping_sub((b as u128) + ((borrow >> 63) as u128)); - (ret as u64, (ret >> 64) as u64) -} - -/// Compute a + (b * c) + carry, returning the result and the new carry over. -#[inline(always)] -pub const fn mac(a: u64, b: u64, c: u64, carry: u64) -> (u64, u64) { - let ret = (a as u128) + ((b as u128) * (c as u128)) + (carry as u128); - (ret as u64, (ret >> 64) as u64) -} - -macro_rules! impl_add_binop_specify_output { - ($lhs:ident, $rhs:ident, $output:ident) => { - impl<'b> Add<&'b $rhs> for $lhs { - type Output = $output; - - #[inline] - fn add(self, rhs: &'b $rhs) -> $output { - &self + rhs - } - } - - impl<'a> Add<$rhs> for &'a $lhs { - type Output = $output; - - #[inline] - fn add(self, rhs: $rhs) -> $output { - self + &rhs - } - } - - impl Add<$rhs> for $lhs { - type Output = $output; - - #[inline] - fn add(self, rhs: $rhs) -> $output { - &self + &rhs - } - } - }; -} - -macro_rules! impl_sub_binop_specify_output { - ($lhs:ident, $rhs:ident, $output:ident) => { - impl<'b> Sub<&'b $rhs> for $lhs { - type Output = $output; - - #[inline] - fn sub(self, rhs: &'b $rhs) -> $output { - &self - rhs - } - } - - impl<'a> Sub<$rhs> for &'a $lhs { - type Output = $output; - - #[inline] - fn sub(self, rhs: $rhs) -> $output { - self - &rhs - } - } - - impl Sub<$rhs> for $lhs { - type Output = $output; - - #[inline] - fn sub(self, rhs: $rhs) -> $output { - &self - &rhs - } - } - }; -} - -macro_rules! impl_binops_additive_specify_output { - ($lhs:ident, $rhs:ident, $output:ident) => { - impl_add_binop_specify_output!($lhs, $rhs, $output); - impl_sub_binop_specify_output!($lhs, $rhs, $output); - }; -} - -macro_rules! impl_binops_multiplicative_mixed { - ($lhs:ident, $rhs:ident, $output:ident) => { - impl<'b> Mul<&'b $rhs> for $lhs { - type Output = $output; - - #[inline] - fn mul(self, rhs: &'b $rhs) -> $output { - &self * rhs - } - } - - impl<'a> Mul<$rhs> for &'a $lhs { - type Output = $output; - - #[inline] - fn mul(self, rhs: $rhs) -> $output { - self * &rhs - } - } - - impl Mul<$rhs> for $lhs { - type Output = $output; - - #[inline] - fn mul(self, rhs: $rhs) -> $output { - &self * &rhs - } - } - }; -} - -macro_rules! impl_binops_additive { - ($lhs:ident, $rhs:ident) => { - impl_binops_additive_specify_output!($lhs, $rhs, $lhs); - - impl SubAssign<$rhs> for $lhs { - #[inline] - fn sub_assign(&mut self, rhs: $rhs) { - *self = &*self - &rhs; - } - } - - impl AddAssign<$rhs> for $lhs { - #[inline] - fn add_assign(&mut self, rhs: $rhs) { - *self = &*self + &rhs; - } - } - - impl<'b> SubAssign<&'b $rhs> for $lhs { - #[inline] - fn sub_assign(&mut self, rhs: &'b $rhs) { - *self = &*self - rhs; - } - } - - impl<'b> AddAssign<&'b $rhs> for $lhs { - #[inline] - fn add_assign(&mut self, rhs: &'b $rhs) { - *self = &*self + rhs; - } - } - }; -} - -macro_rules! impl_binops_multiplicative { - ($lhs:ident, $rhs:ident) => { - impl_binops_multiplicative_mixed!($lhs, $rhs, $lhs); - - impl MulAssign<$rhs> for $lhs { - #[inline] - fn mul_assign(&mut self, rhs: $rhs) { - *self = &*self * &rhs; - } - } - - impl<'b> MulAssign<&'b $rhs> for $lhs { - #[inline] - fn mul_assign(&mut self, rhs: &'b $rhs) { - *self = &*self * rhs; - } - } - }; -} diff --git a/constantine/csharp.patch b/constantine/csharp.patch index 2e8678bcc..91d17ee27 100644 --- a/constantine/csharp.patch +++ b/constantine/csharp.patch @@ -16,7 +16,7 @@ index 5158aad..af3b2a8 100644 INCLUDE_DIRS = ../../src ../../blst/bindings -TARGETS = ckzg.c ../../src/c_kzg_4844.c ../../blst/$(BLST_OBJ) -+TARGETS = ckzg.c ../../../../target/release/rust_kzg_zkcrypto.a ++TARGETS = ckzg.c ../../../../target/release/rust_kzg_blst.a CFLAGS += -O2 -Wall -Wextra -shared CFLAGS += ${addprefix -I,${INCLUDE_DIRS}} diff --git a/constantine/go.patch b/constantine/go.patch index 9ef34988f..dd2de92cc 100644 --- a/constantine/go.patch +++ b/constantine/go.patch @@ -24,7 +24,7 @@ index bdd5385..155fc81 100644 +// #endif +// #include +// #include "c_kzg_4844.h" -+// #cgo LDFLAGS: -L${SRCDIR}/../../../../target/release -l:rust_kzg_zkcrypto.a -lm ++// #cgo LDFLAGS: -L${SRCDIR}/../../../../target/release -l:rust_kzg_blst.a -lm import "C" import ( diff --git a/constantine/java.patch b/constantine/java.patch index a5a7fa44d..978ddb89b 100644 --- a/constantine/java.patch +++ b/constantine/java.patch @@ -15,7 +15,7 @@ index 9be2fd6..1e59378 100644 INCLUDE_DIRS = ../../src ../../blst/bindings -TARGETS=c_kzg_4844_jni.c ../../src/c_kzg_4844.c ../../lib/libblst.a -+TARGETS=c_kzg_4844_jni.c ../../../../target/release/rust_kzg_zkcrypto.a ++TARGETS=c_kzg_4844_jni.c ../../../../target/release/rust_kzg_blst.a CC_FLAGS= OPTIMIZATION_LEVEL=-O2 diff --git a/constantine/nodejs.patch b/constantine/nodejs.patch index 710482442..3e310fd91 100644 --- a/constantine/nodejs.patch +++ b/constantine/nodejs.patch @@ -65,7 +65,7 @@ index 5ac368e..6cde37f 100644 - } - }] + "libraries": [ -+ "<(module_root_dir)/../../../../target/release/rust_kzg_zkcrypto.a" ++ "<(module_root_dir)/../../../../target/release/rust_kzg_blst.a" ] } ] diff --git a/constantine/python.patch b/constantine/python.patch index 01dae992d..c039782de 100644 --- a/constantine/python.patch +++ b/constantine/python.patch @@ -15,7 +15,7 @@ index c6bd222..99d6501 100644 @@ -1,11 +1,8 @@ .PHONY: all all: install test - + -../../src/c_kzg_4844.o: - make -C../../src c_kzg_4844.o - @@ -39,7 +39,7 @@ index b072833..db37db4 100644 - library_dirs=["../../lib"], - libraries=["blst"])]) + library_dirs=["../../lib", "../../../../target/release"], -+ libraries=[":rust_kzg_zkcrypto.a"])]) ++ libraries=[":rust_kzg_blst.a"])]) if __name__ == "__main__": main() diff --git a/constantine/rust.patch b/constantine/rust.patch index 640ab11a0..bd3c42a1f 100644 --- a/constantine/rust.patch +++ b/constantine/rust.patch @@ -53,7 +53,7 @@ index 692305a..e874ccd 100644 // Finally, tell cargo this provides ckzg/ckzg_min - println!("cargo:rustc-link-lib=ckzg"); + println!("cargo:rustc-link-search={}", rust_kzg_target_dir.display()); -+ println!("cargo:rustc-link-arg=-l:rust_kzg_zkcrypto.a"); ++ println!("cargo:rustc-link-arg=-l:rust_kzg_blst.a"); } -fn make_bindings

( diff --git a/constantine/src/consts.rs b/constantine/src/consts.rs index 67b22320e..17d3af1f2 100644 --- a/constantine/src/consts.rs +++ b/constantine/src/consts.rs @@ -1,7 +1,15 @@ -use crate::kzg_types::{ZG1, ZG2}; -use bls12_381::{Fp as ZFp, Fp2 as ZFp2, G1Projective, G2Projective}; +//blst_fp = bls12_381_fp, FsG1 = CtG1, blst_p1 = bls12_381_g1_jac, blst_fr = bls12_381_fr +use constantine_sys::{bls12_381_fp, bls12_381_fp2, bls12_381_g1_jac, bls12_381_g2_jac}; + +use crate::types::g1::CtG1; +use crate::types::g2::CtG2; + +pub const G1_IDENTITY: CtG1 = CtG1::from_xyz(bls12_381_fp { limbs: [0; 6] },bls12_381_fp { limbs: [0; 6] }, bls12_381_fp { limbs: [0; 6] }); + pub const SCALE_FACTOR: u64 = 5; + pub const NUM_ROOTS: usize = 32; +/// The roots of unity. Every root_i equals 1 when raised to the power of (2 ^ i) #[rustfmt::skip] pub const SCALE2_ROOT_OF_UNITY: [[u64; 4]; 32] = [ [0x0000000000000001, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000], @@ -35,180 +43,226 @@ pub const SCALE2_ROOT_OF_UNITY: [[u64; 4]; 32] = [ [0xd7b688830a4f2089, 0x6558e9e3f6ac7b41, 0x99e276b571905a7d, 0x52dd465e2f094256], [0x474650359d8e211b, 0x84d37b826214abc6, 0x8da40c1ef2bb4598, 0x0c83ea7744bf1bee], [0x694341f608c9dd56, 0xed3a181fabb30adc, 0x1339a815da8b398f, 0x2c6d4e4511657e1e], - [0x63e7cb4906ffc93f, 0xf070bb00e28a193d, 0xad1715b02e5713b5, 0x4b5371495990693f], + [0x63e7cb4906ffc93f, 0xf070bb00e28a193d, 0xad1715b02e5713b5, 0x4b5371495990693f] ]; -/** The G1 generator */ -pub const G1_GENERATOR: ZG1 = ZG1::from_g1_projective(G1Projective { - x: ZFp::from_raw_unchecked([ - 0x5cb3_8790_fd53_0c16, - 0x7817_fc67_9976_fff5, - 0x154f_95c7_143b_a1c1, - 0xf0ae_6acd_f3d0_e747, - 0xedce_6ecc_21db_f440, - 0x1201_7741_9e0b_fb75, - ]), - y: ZFp::from_raw_unchecked([ - 0xbaac_93d5_0ce7_2271, - 0x8c22_631a_7918_fd8e, - 0xdd59_5f13_5707_25ce, - 0x51ac_5829_5040_5194, - 0x0e1c_8c3f_ad00_59c0, - 0x0bbc_3efc_5008_a26a, - ]), - z: ZFp::from_raw_unchecked([ - 0x7609_0000_0002_fffd, - 0xebf4_000b_c40c_0002, - 0x5f48_9857_53c7_58ba, - 0x77ce_5853_7052_5745, - 0x5c07_1a97_a256_ec6d, - 0x15f6_5ec3_fa80_e493, - ]), -}); - -pub const G1_NEGATIVE_GENERATOR: ZG1 = ZG1::from_g1_projective(G1Projective { - x: ZFp::from_raw_unchecked([ - 0x5cb3_8790_fd53_0c16, - 0x7817_fc67_9976_fff5, - 0x154f_95c7_143b_a1c1, - 0xf0ae_6acd_f3d0_e747, - 0xedce_6ecc_21db_f440, - 0x1201_7741_9e0b_fb75, - ]), - y: ZFp::from_raw_unchecked([ - 0xff52_6c2a_f318_883a, - 0x9289_9ce4_383b_0270, - 0x89d7_738d_9fa9_d055, - 0x12ca_f35b_a344_c12a, - 0x3cff_1b76_964b_5317, - 0x0e44_d2ed_e977_4430, - ]), - z: ZFp::from_raw_unchecked([ - 0x7609_0000_0002_fffd, - 0xebf4_000b_c40c_0002, - 0x5f48_9857_53c7_58ba, - 0x77ce_5853_7052_5745, - 0x5c07_1a97_a256_ec6d, - 0x15f6_5ec3_fa80_e493, - ]), -}); -#[rustfmt::skip] -pub const G1_IDENTITY: ZG1 = ZG1::from_g1_projective( G1Projective { - x: ZFp::zero(), - y: ZFp::one(), - z: ZFp::zero(), -}); - -pub const G2_GENERATOR: ZG2 = ZG2::from_g2_projective(G2Projective { - x: ZFp2 { - c0: ZFp([ - 0xf5f28fa202940a10, - 0xb3f5fb2687b4961a, - 0xa1a893b53e2ae580, - 0x9894999d1a3caee9, - 0x6f67b7631863366b, - 0x058191924350bcd7, - ]), - c1: ZFp([ - 0xa5a9c0759e23f606, - 0xaaa0c59dbccd60c3, - 0x3bb17e18e2867806, - 0x1b1ab6cc8541b367, - 0xc2b6ed0ef2158547, - 0x11922a097360edf3, - ]), +pub const G1_GENERATOR: CtG1 = CtG1(bls12_381_g1_jac { + x: bls12_381_fp { + limbs: [ + 0x5cb38790fd530c16, + 0x7817fc679976fff5, + 0x154f95c7143ba1c1, + 0xf0ae6acdf3d0e747, + 0xedce6ecc21dbf440, + 0x120177419e0bfb75, + ], }, - y: ZFp2 { - c0: ZFp([ - 0x4c730af860494c4a, - 0x597cfa1f5e369c5a, - 0xe7e6856caa0a635a, - 0xbbefb5e96e0d495f, - 0x07d3a975f0ef25a2, - 0x0083fd8e7e80dae5, - ]), - c1: ZFp([ - 0xadc0fc92df64b05d, - 0x18aa270a2b1461dc, - 0x86adac6a3be4eba0, - 0x79495c4ec93da33a, - 0xe7175850a43ccaed, - 0x0b2bc2a163de1bf2, - ]), + y: bls12_381_fp { + limbs: [ + 0xbaac93d50ce72271, + 0x8c22631a7918fd8e, + 0xdd595f13570725ce, + 0x51ac582950405194, + 0x0e1c8c3fad0059c0, + 0x0bbc3efc5008a26a, + ], }, - z: ZFp2 { - c0: ZFp([ + z: bls12_381_fp { + limbs: [ 0x760900000002fffd, 0xebf4000bc40c0002, 0x5f48985753c758ba, 0x77ce585370525745, 0x5c071a97a256ec6d, 0x15f65ec3fa80e493, - ]), - c1: ZFp([ - 0x0000000000000000, - 0x0000000000000000, - 0x0000000000000000, - 0x0000000000000000, - 0x0000000000000000, - 0x0000000000000000, - ]), + ], }, }); -pub const G2_NEGATIVE_GENERATOR: ZG2 = ZG2::from_g2_projective(G2Projective { - x: ZFp2 { - c0: ZFp([ - 0xf5f28fa202940a10, - 0xb3f5fb2687b4961a, - 0xa1a893b53e2ae580, - 0x9894999d1a3caee9, - 0x6f67b7631863366b, - 0x058191924350bcd7, - ]), - c1: ZFp([ - 0xa5a9c0759e23f606, - 0xaaa0c59dbccd60c3, - 0x3bb17e18e2867806, - 0x1b1ab6cc8541b367, - 0xc2b6ed0ef2158547, - 0x11922a097360edf3, - ]), +pub const G1_NEGATIVE_GENERATOR: CtG1 = CtG1(bls12_381_g1_jac { + x: bls12_381_fp { + limbs: [ + 0x5cb38790fd530c16, + 0x7817fc679976fff5, + 0x154f95c7143ba1c1, + 0xf0ae6acdf3d0e747, + 0xedce6ecc21dbf440, + 0x120177419e0bfb75, + ], }, - y: ZFp2 { - c0: ZFp([ - 0x6d8bf5079fb65e61, - 0xc52f05df531d63a5, - 0x7f4a4d344ca692c9, - 0xa887959b8577c95f, - 0x4347fe40525c8734, - 0x197d145bbaff0bb5, - ]), - c1: ZFp([ - 0x0c3e036d209afa4e, - 0x0601d8f4863f9e23, - 0xe0832636bacc0a84, - 0xeb2def362a476f84, - 0x64044f659f0ee1e9, - 0x0ed54f48d5a1caa7, - ]), + y: bls12_381_fp { + limbs: [ + 0xff526c2af318883a, + 0x92899ce4383b0270, + 0x89d7738d9fa9d055, + 0x12caf35ba344c12a, + 0x3cff1b76964b5317, + 0x0e44d2ede9774430, + ], }, - z: ZFp2 { - c0: ZFp([ + z: bls12_381_fp { + limbs: [ 0x760900000002fffd, 0xebf4000bc40c0002, 0x5f48985753c758ba, 0x77ce585370525745, 0x5c071a97a256ec6d, 0x15f65ec3fa80e493, - ]), - c1: ZFp([ - 0x0000000000000000, - 0x0000000000000000, - 0x0000000000000000, - 0x0000000000000000, - 0x0000000000000000, - 0x0000000000000000, - ]), + ], + }, +}); + +pub const G2_GENERATOR: CtG2 = CtG2(bls12_381_g2_jac { + x: bls12_381_fp2 { + c: [ + bls12_381_fp { + limbs: [ + 0xf5f28fa202940a10, + 0xb3f5fb2687b4961a, + 0xa1a893b53e2ae580, + 0x9894999d1a3caee9, + 0x6f67b7631863366b, + 0x058191924350bcd7, + ], + }, + bls12_381_fp { + limbs: [ + 0xa5a9c0759e23f606, + 0xaaa0c59dbccd60c3, + 0x3bb17e18e2867806, + 0x1b1ab6cc8541b367, + 0xc2b6ed0ef2158547, + 0x11922a097360edf3, + ], + }, + ], + }, + y: bls12_381_fp2 { + c: [ + bls12_381_fp { + limbs: [ + 0x4c730af860494c4a, + 0x597cfa1f5e369c5a, + 0xe7e6856caa0a635a, + 0xbbefb5e96e0d495f, + 0x07d3a975f0ef25a2, + 0x0083fd8e7e80dae5, + ], + }, + bls12_381_fp { + limbs: [ + 0xadc0fc92df64b05d, + 0x18aa270a2b1461dc, + 0x86adac6a3be4eba0, + 0x79495c4ec93da33a, + 0xe7175850a43ccaed, + 0x0b2bc2a163de1bf2, + ], + }, + ], + }, + z: bls12_381_fp2 { + c: [ + bls12_381_fp { + limbs: [ + 0x760900000002fffd, + 0xebf4000bc40c0002, + 0x5f48985753c758ba, + 0x77ce585370525745, + 0x5c071a97a256ec6d, + 0x15f65ec3fa80e493, + ], + }, + bls12_381_fp { + limbs: [ + 0x0000000000000000, + 0x0000000000000000, + 0x0000000000000000, + 0x0000000000000000, + 0x0000000000000000, + 0x0000000000000000, + ], + }, + ], }, }); + +pub const G2_NEGATIVE_GENERATOR: CtG2 = CtG2(bls12_381_g2_jac { + x: bls12_381_fp2 { + c: [ + bls12_381_fp { + limbs: [ + 0xf5f28fa202940a10, + 0xb3f5fb2687b4961a, + 0xa1a893b53e2ae580, + 0x9894999d1a3caee9, + 0x6f67b7631863366b, + 0x058191924350bcd7, + ], + }, + bls12_381_fp { + limbs: [ + 0xa5a9c0759e23f606, + 0xaaa0c59dbccd60c3, + 0x3bb17e18e2867806, + 0x1b1ab6cc8541b367, + 0xc2b6ed0ef2158547, + 0x11922a097360edf3, + ], + }, + ], + }, + y: bls12_381_fp2 { + fp: [ + bls12_381_fp { + limbs: [ + 0x6d8bf5079fb65e61, + 0xc52f05df531d63a5, + 0x7f4a4d344ca692c9, + 0xa887959b8577c95f, + 0x4347fe40525c8734, + 0x197d145bbaff0bb5, + ], + }, + bls12_381_fp { + limbs: [ + 0x0c3e036d209afa4e, + 0x0601d8f4863f9e23, + 0xe0832636bacc0a84, + 0xeb2def362a476f84, + 0x64044f659f0ee1e9, + 0x0ed54f48d5a1caa7, + ], + }, + ], + }, + z: bls12_381_fp2 { + fp: [ + bls12_381_fp { + limbs: [ + 0x760900000002fffd, + 0xebf4000bc40c0002, + 0x5f48985753c758ba, + 0x77ce585370525745, + 0x5c071a97a256ec6d, + 0x15f65ec3fa80e493, + ], + }, + bls12_381_fp { + limbs: [ + 0x0000000000000000, + 0x0000000000000000, + 0x0000000000000000, + 0x0000000000000000, + 0x0000000000000000, + 0x0000000000000000, + ], + }, + ], + }, +}); + +pub const TRUSTED_SETUP_GENERATOR: [u8; 32usize] = [ + 0xa4, 0x73, 0x31, 0x95, 0x28, 0xc8, 0xb6, 0xea, 0x4d, 0x08, 0xcc, 0x53, 0x18, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]; diff --git a/constantine/src/das.rs b/constantine/src/das.rs deleted file mode 100644 index b0867d820..000000000 --- a/constantine/src/das.rs +++ /dev/null @@ -1,87 +0,0 @@ -use crate::kzg_proofs::FFTSettings; -use crate::kzg_types::ZFr as BlstFr; -use kzg::{Fr, DAS}; -use std::cmp::Ordering; - -impl FFTSettings { - fn das_fft_extension_stride(&self, ab: &mut [BlstFr], stride: usize) { - match ab.len().cmp(&2_usize) { - Ordering::Less => {} - Ordering::Greater => { - let half = ab.len(); - let halfhalf = half / 2; - - for i in 0..halfhalf { - let tmp1 = ab[i].add(&ab[halfhalf + i]); - let tmp2 = ab[i].sub(&ab[halfhalf + i]); - ab[halfhalf + i] = tmp2.mul(&self.reverse_roots_of_unity[i * 2 * stride]); - ab[i] = tmp1; - } - - #[cfg(feature = "parallel")] - { - if ab.len() > 32 { - let (lo, hi) = ab.split_at_mut(halfhalf); - rayon::join( - || self.das_fft_extension_stride(hi, stride * 2), - || self.das_fft_extension_stride(lo, stride * 2), - ); - } else { - self.das_fft_extension_stride(&mut ab[..halfhalf], stride * 2); - self.das_fft_extension_stride(&mut ab[halfhalf..], stride * 2); - } - } - #[cfg(not(feature = "parallel"))] - { - self.das_fft_extension_stride(&mut ab[..halfhalf], stride * 2); - self.das_fft_extension_stride(&mut ab[halfhalf..], stride * 2); - } - for i in 0..halfhalf { - let x = ab[i]; - let y = ab[halfhalf + i]; - let y_times_root = y.mul(&self.expanded_roots_of_unity[(1 + 2 * i) * stride]); - ab[i] = x.add(&y_times_root); - ab[halfhalf + i] = x.sub(&y_times_root); - } - } - Ordering::Equal => { - let x = ab[0].add(&ab[1]); - let y = ab[0].sub(&ab[1]); - let tmp = y.mul(&self.expanded_roots_of_unity[stride]); - - ab[0] = x.add(&tmp); - ab[1] = x.sub(&tmp); - } - } - } -} - -impl DAS for FFTSettings { - fn das_fft_extension(&self, vals: &[BlstFr]) -> Result, String> { - if vals.is_empty() { - return Err(String::from("vals can not be empty")); - } - if !vals.len().is_power_of_two() { - return Err(String::from("vals lenght has to be power of 2")); - } - if vals.len() * 2 > self.max_width { - return Err(String::from( - "vals lenght * 2 has to equal or less than FFTSetings max width", - )); - } - - let mut vals = vals.to_vec(); - let stride = self.max_width / (vals.len() * 2); - - self.das_fft_extension_stride(&mut vals, stride); - - let invlen = BlstFr::from_u64(vals.len() as u64); - let invlen = invlen.inverse(); - - for val in &mut vals { - val.fr *= invlen.fr - } - - Ok(vals) - } -} diff --git a/constantine/src/data_availability_sampling.rs b/constantine/src/data_availability_sampling.rs new file mode 100644 index 000000000..f2134e580 --- /dev/null +++ b/constantine/src/data_availability_sampling.rs @@ -0,0 +1,102 @@ +//blst_fp = bls12_381_fp, FsG1 = CtG1, blst_p1 = bls12_381_g1_jacbls12_381_g1_jac +extern crate alloc; + +use alloc::string::String; +use alloc::vec::Vec; +use core::cmp::Ordering; + +use kzg::{Fr, DAS}; + +use crate::types::fft_settings::CtFFTSettings; +use crate::types::fr::CtFr; + +// TODO: explain algo +impl CtFFTSettings { + pub fn das_fft_extension_stride(&self, evens: &mut [CtFr], stride: usize) { + match evens.len().cmp(&2) { + Ordering::Less => { + return; + } + Ordering::Equal => { + let x = evens[0].add(&evens[1]); + let y = evens[0].sub(&evens[1]); + let y_times_root = y.mul(&self.expanded_roots_of_unity[stride]); + + evens[0] = x.add(&y_times_root); + evens[1] = x.sub(&y_times_root); + + return; + } + Ordering::Greater => {} + } + + let half: usize = evens.len() / 2; + for i in 0..half { + let tmp1 = evens[i].add(&evens[half + i]); + let tmp2 = evens[i].sub(&evens[half + i]); + evens[half + i] = tmp2.mul(&self.reverse_roots_of_unity[i * 2 * stride]); + + evens[i] = tmp1; + } + + #[cfg(feature = "parallel")] + { + if evens.len() > 32 { + let (lo, hi) = evens.split_at_mut(half); + rayon::join( + || self.das_fft_extension_stride(hi, stride * 2), + || self.das_fft_extension_stride(lo, stride * 2), + ); + } else { + // Recurse + self.das_fft_extension_stride(&mut evens[..half], stride * 2); + self.das_fft_extension_stride(&mut evens[half..], stride * 2); + } + } + + #[cfg(not(feature = "parallel"))] + { + // Recurse + self.das_fft_extension_stride(&mut evens[..half], stride * 2); + self.das_fft_extension_stride(&mut evens[half..], stride * 2); + } + + for i in 0..half { + let x = evens[i]; + let y = evens[half + i]; + let y_times_root: CtFr = y.mul(&self.expanded_roots_of_unity[(1 + 2 * i) * stride]); + + evens[i] = x.add(&y_times_root); + evens[half + i] = x.sub(&y_times_root); + } + } +} + +impl DAS for CtFFTSettings { + /// Polynomial extension for data availability sampling. Given values of even indices, produce values of odd indices. + /// FFTSettings must hold at least 2 times the roots of provided evens. + /// The resulting odd indices make the right half of the coefficients of the inverse FFT of the combined indices zero. + fn das_fft_extension(&self, evens: &[CtFr]) -> Result, String> { + if evens.is_empty() { + return Err(String::from("A non-zero list ab expected")); + } else if !evens.len().is_power_of_two() { + return Err(String::from("A list with power-of-two length expected")); + } else if evens.len() * 2 > self.max_width { + return Err(String::from( + "Supplied list is longer than the available max width", + )); + } + + // In case more roots are provided with fft_settings, use a larger stride + let stride = self.max_width / (evens.len() * 2); + let mut odds = evens.to_vec(); + self.das_fft_extension_stride(&mut odds, stride); + + // TODO: explain why each odd member is multiplied by euclidean inverse of length + let mut inv_len = CtFr::from_u64(odds.len() as u64); + inv_len = inv_len.eucl_inverse(); + let odds = odds.iter().map(|f| f.mul(&inv_len)).collect(); + + Ok(odds) + } +} diff --git a/constantine/src/eip_4844.rs b/constantine/src/eip_4844.rs index eadc496ff..b8e67c52b 100644 --- a/constantine/src/eip_4844.rs +++ b/constantine/src/eip_4844.rs @@ -1,20 +1,16 @@ extern crate alloc; -use crate::kzg_proofs::{FFTSettings, KZGSettings}; -use crate::kzg_types::{ZFr, ZG1, ZG2}; -use blst::{blst_fr, blst_p1, blst_p2}; +use alloc::boxed::Box; +use alloc::string::String; +use alloc::vec::Vec; +use core::ptr::null_mut; use kzg::common_utils::reverse_bit_order; use kzg::eip_4844::{ blob_to_kzg_commitment_rust, compute_blob_kzg_proof_rust, compute_kzg_proof_rust, load_trusted_setup_rust, verify_blob_kzg_proof_batch_rust, verify_blob_kzg_proof_rust, - verify_kzg_proof_rust, Blob, Bytes32, Bytes48, CKZGSettings, KZGCommitment, KZGProof, - BYTES_PER_FIELD_ELEMENT, BYTES_PER_G1, BYTES_PER_G2, C_KZG_RET, C_KZG_RET_BADARGS, - C_KZG_RET_OK, FIELD_ELEMENTS_PER_BLOB, TRUSTED_SETUP_NUM_G1_POINTS, - TRUSTED_SETUP_NUM_G2_POINTS, + verify_kzg_proof_rust, }; use kzg::{cfg_into_iter, Fr, G1}; -use std::ptr::null_mut; - #[cfg(feature = "std")] use libc::FILE; #[cfg(feature = "std")] @@ -22,14 +18,29 @@ use std::fs::File; #[cfg(feature = "std")] use std::io::Read; -#[cfg(feature = "parallel")] -use rayon::prelude::*; - #[cfg(feature = "std")] use kzg::eip_4844::load_trusted_setup_string; +use kzg::eip_4844::{ + Blob, Bytes32, Bytes48, CKZGSettings, KZGCommitment, KZGProof, BYTES_PER_FIELD_ELEMENT, + BYTES_PER_G1, BYTES_PER_G2, C_KZG_RET, C_KZG_RET_BADARGS, C_KZG_RET_OK, + FIELD_ELEMENTS_PER_BLOB, TRUSTED_SETUP_NUM_G1_POINTS, TRUSTED_SETUP_NUM_G2_POINTS, +}; + +use crate::types::fft_settings::CtFFTSettings; +use crate::types::fr::CtFr; +use crate::types::g1::CtG1; + +use crate::types::g2::CtG2; +use crate::types::kzg_settings::CtKZGSettings; + +use constantine_sys::{bls12_381_g1_jac, bls12_381_g2_jac, bls12_381_fr}; + +#[cfg(feature = "parallel")] +use rayon::prelude::*; + #[cfg(feature = "std")] -pub fn load_trusted_setup_filename_rust(filepath: &str) -> Result { +pub fn load_trusted_setup_filename_rust(filepath: &str) -> Result { let mut file = File::open(filepath).map_err(|_| "Unable to open file".to_string())?; let mut contents = String::new(); file.read_to_string(&mut contents) @@ -39,17 +50,18 @@ pub fn load_trusted_setup_filename_rust(filepath: &str) -> Result Result { +fn fft_settings_to_rust(c_settings: *const CKZGSettings) -> Result { let settings = unsafe { &*c_settings }; + let roots_of_unity = unsafe { core::slice::from_raw_parts(settings.roots_of_unity, settings.max_width as usize) .iter() - .map(|r| ZFr::from_blst_fr(*r)) - .collect::>() + .map(|r| CtFr(*r)) + .collect::>() }; let mut expanded_roots_of_unity = roots_of_unity.clone(); reverse_bit_order(&mut expanded_roots_of_unity)?; - expanded_roots_of_unity.push(ZFr::one()); + expanded_roots_of_unity.push(CtFr::one()); let mut reverse_roots_of_unity = expanded_roots_of_unity.clone(); reverse_roots_of_unity.reverse(); @@ -57,7 +69,7 @@ fn fft_settings_to_rust(c_settings: *const CKZGSettings) -> Result Result Result { +fn kzg_settings_to_rust(c_settings: &CKZGSettings) -> Result { let secret_g1 = unsafe { core::slice::from_raw_parts(c_settings.g1_values, TRUSTED_SETUP_NUM_G1_POINTS) .iter() - .map(|r| ZG1::from_blst_p1(*r)) - .collect::>() - }; - let secret_g2 = unsafe { - core::slice::from_raw_parts(c_settings.g2_values, TRUSTED_SETUP_NUM_G2_POINTS) - .iter() - .map(|r| ZG2::from_blst_p2(*r)) - .collect::>() + .map(|r| CtG1(*r)) + .collect::>() }; - Ok(KZGSettings { + Ok(CtKZGSettings { fs: fft_settings_to_rust(c_settings)?, secret_g1, - secret_g2, + secret_g2: unsafe { + core::slice::from_raw_parts(c_settings.g2_values, TRUSTED_SETUP_NUM_G2_POINTS) + .iter() + .map(|r| CtG2(*r)) + .collect::>() + }, }) } -fn kzg_settings_to_c(rust_settings: &KZGSettings) -> CKZGSettings { +fn kzg_settings_to_c(rust_settings: &CtKZGSettings) -> CKZGSettings { let g1_val = rust_settings .secret_g1 .iter() - .map(|r| r.to_blst_p1()) - .collect::>(); + .map(|r| r.0) + .collect::>(); let g1_val = Box::new(g1_val); let g2_val = rust_settings .secret_g2 .iter() - .map(|r| r.to_blst_p2()) - .collect::>(); + .map(|r| r.0) + .collect::>(); let x = g2_val.into_boxed_slice(); let stat_ref = Box::leak(x); let v = Box::into_raw(g1_val); @@ -107,8 +118,8 @@ fn kzg_settings_to_c(rust_settings: &KZGSettings) -> CKZGSettings { .fs .roots_of_unity .iter() - .map(|r| r.to_blst_fr()) - .collect::>(), + .map(|r| r.0) + .collect::>(), ); CKZGSettings { @@ -119,20 +130,20 @@ fn kzg_settings_to_c(rust_settings: &KZGSettings) -> CKZGSettings { } } -unsafe fn deserialize_blob(blob: *const Blob) -> Result, C_KZG_RET> { +unsafe fn deserialize_blob(blob: *const Blob) -> Result, C_KZG_RET> { (*blob) .bytes .chunks(BYTES_PER_FIELD_ELEMENT) .map(|chunk| { let mut bytes = [0u8; BYTES_PER_FIELD_ELEMENT]; bytes.copy_from_slice(chunk); - if let Ok(result) = ZFr::from_bytes(&bytes) { + if let Ok(result) = CtFr::from_bytes(&bytes) { Ok(result) } else { Err(C_KZG_RET_BADARGS) } }) - .collect::, C_KZG_RET>>() + .collect::, C_KZG_RET>>() } macro_rules! handle_ckzg_badargs { @@ -209,6 +220,31 @@ pub unsafe extern "C" fn load_trusted_setup_file( C_KZG_RET_OK } +/// # Safety +#[no_mangle] +pub unsafe extern "C" fn compute_blob_kzg_proof( + out: *mut KZGProof, + blob: *const Blob, + commitment_bytes: *const Bytes48, + s: &CKZGSettings, +) -> C_KZG_RET { + let deserialized_blob = match deserialize_blob(blob) { + Ok(value) => value, + Err(err) => return err, + }; + + let commitment_g1 = handle_ckzg_badargs!(CtG1::from_bytes(&(*commitment_bytes).bytes)); + let settings = handle_ckzg_badargs!(kzg_settings_to_rust(s)); + let proof = handle_ckzg_badargs!(compute_blob_kzg_proof_rust( + &deserialized_blob, + &commitment_g1, + &settings + )); + + (*out).bytes = proof.to_bytes(); + C_KZG_RET_OK +} + /// # Safety #[no_mangle] pub unsafe extern "C" fn free_trusted_setup(s: *mut CKZGSettings) { @@ -250,10 +286,10 @@ pub unsafe extern "C" fn verify_kzg_proof( proof_bytes: *const Bytes48, s: &CKZGSettings, ) -> C_KZG_RET { - let frz = handle_ckzg_badargs!(ZFr::from_bytes(&(*z_bytes).bytes)); - let fry = handle_ckzg_badargs!(ZFr::from_bytes(&(*y_bytes).bytes)); - let g1commitment = handle_ckzg_badargs!(ZG1::from_bytes(&(*commitment_bytes).bytes)); - let g1proof = handle_ckzg_badargs!(ZG1::from_bytes(&(*proof_bytes).bytes)); + let frz = handle_ckzg_badargs!(CtFr::from_bytes(&(*z_bytes).bytes)); + let fry = handle_ckzg_badargs!(CtFr::from_bytes(&(*y_bytes).bytes)); + let g1commitment = handle_ckzg_badargs!(CtG1::from_bytes(&(*commitment_bytes).bytes)); + let g1proof = handle_ckzg_badargs!(CtG1::from_bytes(&(*proof_bytes).bytes)); let settings = handle_ckzg_badargs!(kzg_settings_to_rust(s)); @@ -279,10 +315,8 @@ pub unsafe extern "C" fn verify_blob_kzg_proof( s: &CKZGSettings, ) -> C_KZG_RET { let deserialized_blob = handle_ckzg_badargs!(deserialize_blob(blob)); - - let commitment_g1 = handle_ckzg_badargs!(ZG1::from_bytes(&(*commitment_bytes).bytes)); - let proof_g1 = handle_ckzg_badargs!(ZG1::from_bytes(&(*proof_bytes).bytes)); - + let commitment_g1 = handle_ckzg_badargs!(CtG1::from_bytes(&(*commitment_bytes).bytes)); + let proof_g1 = handle_ckzg_badargs!(CtG1::from_bytes(&(*proof_bytes).bytes)); let settings = handle_ckzg_badargs!(kzg_settings_to_rust(s)); let result = handle_ckzg_badargs!(verify_blob_kzg_proof_rust( @@ -310,16 +344,18 @@ pub unsafe extern "C" fn verify_blob_kzg_proof_batch( let raw_commitments = core::slice::from_raw_parts(commitments_bytes, n); let raw_proofs = core::slice::from_raw_parts(proofs_bytes, n); - let deserialized_blobs: Result>, C_KZG_RET> = cfg_into_iter!(raw_blobs) + let deserialized_blobs: Result>, C_KZG_RET> = cfg_into_iter!(raw_blobs) .map(|raw_blob| deserialize_blob(raw_blob).map_err(|_| C_KZG_RET_BADARGS)) .collect(); - let commitments_g1: Result, C_KZG_RET> = cfg_into_iter!(raw_commitments) - .map(|raw_commitment| ZG1::from_bytes(&raw_commitment.bytes).map_err(|_| C_KZG_RET_BADARGS)) + let commitments_g1: Result, C_KZG_RET> = cfg_into_iter!(raw_commitments) + .map(|raw_commitment| { + CtG1::from_bytes(&raw_commitment.bytes).map_err(|_| C_KZG_RET_BADARGS) + }) .collect(); - let proofs_g1: Result, C_KZG_RET> = cfg_into_iter!(raw_proofs) - .map(|raw_proof| ZG1::from_bytes(&raw_proof.bytes).map_err(|_| C_KZG_RET_BADARGS)) + let proofs_g1: Result, C_KZG_RET> = cfg_into_iter!(raw_proofs) + .map(|raw_proof| CtG1::from_bytes(&raw_proof.bytes).map_err(|_| C_KZG_RET_BADARGS)) .collect(); if let (Ok(blobs), Ok(commitments), Ok(proofs)) = @@ -345,31 +381,6 @@ pub unsafe extern "C" fn verify_blob_kzg_proof_batch( } } -/// # Safety -#[no_mangle] -pub unsafe extern "C" fn compute_blob_kzg_proof( - out: *mut KZGProof, - blob: *const Blob, - commitment_bytes: *const Bytes48, - s: &CKZGSettings, -) -> C_KZG_RET { - let deserialized_blob = match deserialize_blob(blob) { - Ok(value) => value, - Err(err) => return err, - }; - - let commitment_g1 = handle_ckzg_badargs!(ZG1::from_bytes(&(*commitment_bytes).bytes)); - let settings = handle_ckzg_badargs!(kzg_settings_to_rust(s)); - let proof = handle_ckzg_badargs!(compute_blob_kzg_proof_rust( - &deserialized_blob, - &commitment_g1, - &settings - )); - - (*out).bytes = proof.to_bytes(); - C_KZG_RET_OK -} - /// # Safety #[no_mangle] pub unsafe extern "C" fn compute_kzg_proof( @@ -384,7 +395,7 @@ pub unsafe extern "C" fn compute_kzg_proof( Err(err) => return err, }; - let frz = match ZFr::from_bytes(&(*z_bytes).bytes) { + let frz = match CtFr::from_bytes(&(*z_bytes).bytes) { Ok(value) => value, Err(_) => return C_KZG_RET_BADARGS, }; @@ -404,3 +415,40 @@ pub unsafe extern "C" fn compute_kzg_proof( (*y_out).bytes = fry_tmp.to_bytes(); C_KZG_RET_OK } + +#[cfg(test)] +mod tests { + use kzg_bench::tests::utils::get_trusted_setup_path; + + use crate::eip_4844::{kzg_settings_to_c, kzg_settings_to_rust}; + + use super::load_trusted_setup_filename_rust; + + #[test] + fn kzg_settings_to_rust_check_conversion() { + let settings = load_trusted_setup_filename_rust(get_trusted_setup_path().as_str()); + + assert!(settings.is_ok()); + + let settings = settings.unwrap(); + + let converted_settings = kzg_settings_to_rust(&kzg_settings_to_c(&settings)).unwrap(); + + assert_eq!( + settings.fs.root_of_unity, + converted_settings.fs.root_of_unity + ); + assert_eq!( + settings.fs.roots_of_unity, + converted_settings.fs.roots_of_unity + ); + assert_eq!( + settings.fs.expanded_roots_of_unity, + converted_settings.fs.expanded_roots_of_unity + ); + assert_eq!( + settings.fs.reverse_roots_of_unity, + converted_settings.fs.reverse_roots_of_unity + ); + } +} diff --git a/constantine/src/fft.rs b/constantine/src/fft.rs deleted file mode 100644 index db3fb7dfb..000000000 --- a/constantine/src/fft.rs +++ /dev/null @@ -1,106 +0,0 @@ -use crate::kzg_proofs::FFTSettings; -use crate::kzg_types::ZFr as BlstFr; -use kzg::{FFTFr, Fr as FFr}; - -impl FFTFr for FFTSettings { - fn fft_fr(&self, data: &[BlstFr], inverse: bool) -> Result, String> { - if data.len() > self.max_width { - return Err(String::from("data length is longer than allowed max width")); - } - if !data.len().is_power_of_two() { - return Err(String::from("data length is not power of 2")); - } - - let stride = self.max_width / data.len(); - let mut ret = vec![BlstFr::default(); data.len()]; - - let roots = if inverse { - &self.reverse_roots_of_unity - } else { - &self.expanded_roots_of_unity - }; - - fft_fr_fast(&mut ret, data, 1, roots, stride); - - if inverse { - let inv_fr_len = BlstFr::from_u64(data.len() as u64).inverse(); - ret[..data.len()] - .iter_mut() - .for_each(|f| *f = BlstFr::mul(f, &inv_fr_len)); - } - - Ok(ret) - } -} -pub fn fft_fr_fast( - ret: &mut [BlstFr], - data: &[BlstFr], - stride: usize, - roots: &[BlstFr], - roots_stride: usize, -) { - let half: usize = ret.len() / 2; - if half > 0 { - #[cfg(not(feature = "parallel"))] - { - fft_fr_fast(&mut ret[..half], data, stride * 2, roots, roots_stride * 2); - fft_fr_fast( - &mut ret[half..], - &data[stride..], - stride * 2, - roots, - roots_stride * 2, - ); - } - - #[cfg(feature = "parallel")] - { - if half > 256 { - let (lo, hi) = ret.split_at_mut(half); - rayon::join( - || fft_fr_fast(lo, data, stride * 2, roots, roots_stride * 2), - || fft_fr_fast(hi, &data[stride..], stride * 2, roots, roots_stride * 2), - ); - } else { - fft_fr_fast(&mut ret[..half], data, stride * 2, roots, roots_stride * 2); - fft_fr_fast( - &mut ret[half..], - &data[stride..], - stride * 2, - roots, - roots_stride * 2, - ); - } - } - - for i in 0..half { - let y_times_root = ret[i + half].mul(&roots[i * roots_stride]); - ret[i + half] = ret[i].sub(&y_times_root); - ret[i] = ret[i].add(&y_times_root); - } - } else { - ret[0] = data[0]; - } -} - -pub fn fft_fr_slow( - ret: &mut [BlstFr], - data: &[BlstFr], - stride: usize, - roots: &[BlstFr], - roots_stride: usize, -) { - let mut v; - let mut jv; - let mut r; - - for i in 0..data.len() { - ret[i] = data[0].mul(&roots[0]); - for j in 1..data.len() { - jv = data[j * stride]; - r = roots[((i * j) % data.len()) * roots_stride]; - v = jv.mul(&r); - ret[i] = ret[i].add(&v); - } - } -} diff --git a/constantine/src/fft_fr.rs b/constantine/src/fft_fr.rs new file mode 100644 index 000000000..0341eef52 --- /dev/null +++ b/constantine/src/fft_fr.rs @@ -0,0 +1,145 @@ +extern crate alloc; + +use alloc::format; +use alloc::string::String; +use alloc::vec; +use alloc::vec::Vec; + +use kzg::{FFTFr, Fr}; + +use crate::types::fft_settings::CtFFTSettings; +use crate::types::fr::CtFr; + +/// Fast Fourier Transform for finite field elements. Polynomial ret is operated on in reverse order: ret_i * x ^ (len - i - 1) +pub fn fft_fr_fast( + ret: &mut [CtFr], + data: &[CtFr], + stride: usize, + roots: &[CtFr], + roots_stride: usize, +) { + let half: usize = ret.len() / 2; + if half > 0 { + // Recurse + // Offsetting data by stride = 1 on the first iteration forces the even members to the first half + // and the odd members to the second half + #[cfg(not(feature = "parallel"))] + { + fft_fr_fast(&mut ret[..half], data, stride * 2, roots, roots_stride * 2); + fft_fr_fast( + &mut ret[half..], + &data[stride..], + stride * 2, + roots, + roots_stride * 2, + ); + } + + #[cfg(feature = "parallel")] + { + if half > 256 { + let (lo, hi) = ret.split_at_mut(half); + rayon::join( + || fft_fr_fast(lo, data, stride * 2, roots, roots_stride * 2), + || fft_fr_fast(hi, &data[stride..], stride * 2, roots, roots_stride * 2), + ); + } else { + fft_fr_fast(&mut ret[..half], data, stride * 2, roots, roots_stride * 2); + fft_fr_fast( + &mut ret[half..], + &data[stride..], + stride * 2, + roots, + roots_stride * 2, + ); + } + } + + for i in 0..half { + let y_times_root = ret[i + half].mul(&roots[i * roots_stride]); + ret[i + half] = ret[i].sub(&y_times_root); + ret[i] = ret[i].add(&y_times_root); + } + } else { + // When len = 1, return the permuted element + ret[0] = data[0]; + } +} + +impl CtFFTSettings { + /// Fast Fourier Transform for finite field elements, `output` must be zeroes + pub(crate) fn fft_fr_output( + &self, + data: &[CtFr], + inverse: bool, + output: &mut [CtFr], + ) -> Result<(), String> { + if data.len() > self.max_width { + return Err(String::from( + "Supplied list is longer than the available max width", + )); + } + if data.len() != output.len() { + return Err(format!( + "Output length {} doesn't match data length {}", + data.len(), + output.len() + )); + } + if !data.len().is_power_of_two() { + return Err(String::from("A list with power-of-two length expected")); + } + + // In case more roots are provided with fft_settings, use a larger stride + let stride = self.max_width / data.len(); + + // Inverse is same as regular, but all constants are reversed and results are divided by n + // This is a property of the DFT matrix + let roots = if inverse { + &self.reverse_roots_of_unity + } else { + &self.expanded_roots_of_unity + }; + + fft_fr_fast(output, data, 1, roots, stride); + + if inverse { + let inv_fr_len = CtFr::from_u64(data.len() as u64).inverse(); + output.iter_mut().for_each(|f| *f = f.mul(&inv_fr_len)); + } + + Ok(()) + } +} + +impl FFTFr for CtFFTSettings { + /// Fast Fourier Transform for finite field elements + fn fft_fr(&self, data: &[CtFr], inverse: bool) -> Result, String> { + let mut ret = vec![CtFr::default(); data.len()]; + + self.fft_fr_output(data, inverse, &mut ret)?; + + Ok(ret) + } +} + +/// Simplified Discrete Fourier Transform, mainly used for testing +pub fn fft_fr_slow( + ret: &mut [CtFr], + data: &[CtFr], + stride: usize, + roots: &[CtFr], + roots_stride: usize, +) { + for i in 0..data.len() { + // Evaluate first member at 1 + ret[i] = data[0].mul(&roots[0]); + + // Evaluate the rest of members using a step of (i * J) % data.len() over the roots + // This distributes the roots over correct x^n members and saves on multiplication + for j in 1..data.len() { + let v = data[j * stride].mul(&roots[((i * j) % data.len()) * roots_stride]); + ret[i] = ret[i].add(&v); + } + } +} diff --git a/constantine/src/fft_g1.rs b/constantine/src/fft_g1.rs index 8299866d2..a5c180106 100644 --- a/constantine/src/fft_g1.rs +++ b/constantine/src/fft_g1.rs @@ -1,84 +1,21 @@ -use crate::consts::G1_GENERATOR; -use crate::kzg_proofs::FFTSettings; -use crate::kzg_types::{ZFr, ZG1}; -use crate::multiscalar_mul::msm_variable_base; -use kzg::{Fr as KzgFr, G1Mul}; -use kzg::{FFTG1, G1}; -use std::ops::MulAssign; +extern crate alloc; -#[warn(unused_variables)] -pub fn g1_linear_combination(out: &mut ZG1, points: &[ZG1], scalars: &[ZFr], _len: usize) { - let g1 = msm_variable_base(points, scalars); - out.proj = g1 -} -pub fn make_data(data: usize) -> Vec { - let mut vec = Vec::new(); - if data != 0 { - vec.push(G1_GENERATOR); - for i in 1..data as u64 { - let res = vec[(i - 1) as usize].add_or_dbl(&G1_GENERATOR); - vec.push(res); - } - } - vec -} - -impl FFTG1 for FFTSettings { - fn fft_g1(&self, data: &[ZG1], inverse: bool) -> Result, String> { - if data.len() > self.max_width { - return Err(String::from("data length is longer than allowed max width")); - } - if !data.len().is_power_of_two() { - return Err(String::from("data length is not power of 2")); - } - - let stride: usize = self.max_width / data.len(); - let mut ret = vec![ZG1::default(); data.len()]; +use alloc::string::String; +use alloc::vec; +use alloc::vec::Vec; - let roots = if inverse { - &self.reverse_roots_of_unity - } else { - &self.expanded_roots_of_unity - }; - - fft_g1_fast(&mut ret, data, 1, roots, stride, 1); - - if inverse { - let inv_fr_len = ZFr::from_u64(data.len() as u64).inverse(); - ret[..data.len()] - .iter_mut() - .for_each(|f| f.proj.mul_assign(&inv_fr_len.fr)); - } - Ok(ret) - } -} +use kzg::{Fr, G1Mul, FFTG1, G1}; -pub fn fft_g1_slow( - ret: &mut [ZG1], - data: &[ZG1], - stride: usize, - roots: &[ZFr], - roots_stride: usize, - _width: usize, -) { - for i in 0..data.len() { - ret[i] = data[0].mul(&roots[0]); - for j in 1..data.len() { - let jv = data[j * stride]; - let r = roots[((i * j) % data.len()) * roots_stride]; - let v = jv.mul(&r); - ret[i] = ret[i].add_or_dbl(&v); - } - } -} +use crate::types::fft_settings::CtFFTSettings; +use crate::types::fr::CtFr; +use crate::types::g1::CtG1; pub fn fft_g1_fast( - ret: &mut [ZG1], - data: &[ZG1], + ret: &mut [CtG1], + data: &[CtG1], stride: usize, - roots: &[ZFr], + roots: &[CtFr], roots_stride: usize, - _width: usize, ) { let half = ret.len() / 2; if half > 0 { @@ -86,28 +23,20 @@ pub fn fft_g1_fast( { let (lo, hi) = ret.split_at_mut(half); rayon::join( - || fft_g1_fast(hi, &data[stride..], stride * 2, roots, roots_stride * 2, 1), - || fft_g1_fast(lo, data, stride * 2, roots, roots_stride * 2, 1), + || fft_g1_fast(lo, data, stride * 2, roots, roots_stride * 2), + || fft_g1_fast(hi, &data[stride..], stride * 2, roots, roots_stride * 2), ); } #[cfg(not(feature = "parallel"))] { - fft_g1_fast( - &mut ret[..half], - data, - stride * 2, - roots, - roots_stride * 2, - 1, - ); + fft_g1_fast(&mut ret[..half], data, stride * 2, roots, roots_stride * 2); fft_g1_fast( &mut ret[half..], &data[stride..], stride * 2, roots, roots_stride * 2, - 1, ); } @@ -120,3 +49,56 @@ pub fn fft_g1_fast( ret[0] = data[0]; } } + +impl FFTG1 for CtFFTSettings { + fn fft_g1(&self, data: &[CtG1], inverse: bool) -> Result, String> { + if data.len() > self.max_width { + return Err(String::from( + "Supplied list is longer than the available max width", + )); + } else if !data.len().is_power_of_two() { + return Err(String::from("A list with power-of-two length expected")); + } + + let stride = self.max_width / data.len(); + let mut ret = vec![CtG1::default(); data.len()]; + + let roots = if inverse { + &self.reverse_roots_of_unity + } else { + &self.expanded_roots_of_unity + }; + + fft_g1_fast(&mut ret, data, 1, roots, stride); + + if inverse { + let inv_fr_len = CtFr::from_u64(data.len() as u64).inverse(); + ret[..data.len()] + .iter_mut() + .for_each(|f| *f = f.mul(&inv_fr_len)); + } + + Ok(ret) + } +} + +// Used for testing +pub fn fft_g1_slow( + ret: &mut [CtG1], + data: &[CtG1], + stride: usize, + roots: &[CtFr], + roots_stride: usize, +) { + for i in 0..data.len() { + // Evaluate first member at 1 + ret[i] = data[0].mul(&roots[0]); + + // Evaluate the rest of members using a step of (i * J) % data.len() over the roots + // This distributes the roots over correct x^n members and saves on multiplication + for j in 1..data.len() { + let v = data[j * stride].mul(&roots[((i * j) % data.len()) * roots_stride]); + ret[i] = ret[i].add_or_dbl(&v); + } + } +} diff --git a/constantine/src/fk20_proofs.rs b/constantine/src/fk20_proofs.rs index ec3917f63..21301f9fa 100644 --- a/constantine/src/fk20_proofs.rs +++ b/constantine/src/fk20_proofs.rs @@ -1,324 +1,93 @@ -use crate::consts::G1_IDENTITY; -use crate::kzg_proofs::{FFTSettings, KZGSettings}; -use crate::kzg_types::{ZFr as BlstFr, ZG1, ZG2}; -use crate::poly::PolyData; -use kzg::common_utils::reverse_bit_order; -use kzg::{FFTFr, FK20MultiSettings, FK20SingleSettings, Fr, G1Mul, Poly, FFTG1, G1}; +extern crate alloc; -#[cfg(feature = "parallel")] -use rayon::prelude::*; - -#[repr(C)] -#[derive(Debug, Clone, Default)] -pub struct KzgFK20SingleSettings { - pub ks: KZGSettings, - pub x_ext_fft: Vec, - pub x_ext_fft_len: usize, -} -#[repr(C)] -#[derive(Debug, Clone, Default)] -pub struct KzgFK20MultiSettings { - pub ks: KZGSettings, - pub chunk_len: usize, - pub x_ext_fft_files: Vec>, - pub length: usize, -} +use alloc::vec; +use alloc::vec::Vec; -impl FK20SingleSettings - for KzgFK20SingleSettings -{ - fn new(ks: &KZGSettings, n2: usize) -> Result { - let n = n2 / 2; +use kzg::{FFTFr, Fr, G1Mul, Poly, FFTG1, G1}; - if n2 > ks.fs.max_width { - return Err(String::from( - "n2 must be equal or less than kzg settings max width", - )); - } - if !n2.is_power_of_two() { - return Err(String::from("n2 must be power of 2")); - } - if n2 < 2 { - return Err(String::from("n2 must be equal or greater than 2")); - } +use crate::types::fft_settings::CtFFTSettings; +use crate::types::fr::CtFr; +use crate::types::g1::CtG1; +use crate::types::poly::CtPoly; - let mut x = Vec::new(); - for i in 0..(n - 1) { - x.push(ks.secret_g1[n - 2 - i]) - } - x.push(G1_IDENTITY); - - let new_ks = KZGSettings { - fs: ks.fs.clone(), - ..KZGSettings::default() - }; - - Ok(KzgFK20SingleSettings { - ks: new_ks, - x_ext_fft: toeplitz_part_1(&x, &ks.fs).unwrap(), - x_ext_fft_len: n2, - }) - } +#[cfg(feature = "parallel")] +use rayon::prelude::*; - fn data_availability(&self, p: &PolyData) -> Result, String> { - let n = p.len(); +impl CtFFTSettings { + pub fn toeplitz_part_1(&self, x: &[CtG1]) -> Vec { + let n = x.len(); let n2 = n * 2; + let mut x_ext = Vec::with_capacity(n2); - if n2 > self.ks.fs.max_width { - return Err(String::from( - "n2 must be equal or less than kzg settings max width", - )); - } - if !n.is_power_of_two() { - return Err(String::from("n2 must be power of 2")); - } - - let mut out = fk20_single_da_opt(p, self).unwrap(); - reverse_bit_order(&mut out)?; - Ok(out) - } + x_ext.extend(x.iter().take(n)); + x_ext.resize(n2, CtG1::identity()); - fn data_availability_optimized(&self, p: &PolyData) -> Result, String> { - fk20_single_da_opt(p, self) + self.fft_g1(&x_ext, false).unwrap() } -} - -impl FK20MultiSettings - for KzgFK20MultiSettings -{ - fn new(ks: &KZGSettings, n2: usize, chunk_len: usize) -> Result { - if n2 > ks.fs.max_width { - return Err(String::from( - "n2 must be equal or less than kzg settings max width", - )); - } - if !n2.is_power_of_two() { - return Err(String::from("n2 must be power of 2")); - } - if n2 < 2 { - return Err(String::from("n2 must be equal or greater than 2")); - } - if chunk_len > n2 / 2 { - return Err(String::from("chunk_len must be equal or less than n2/2")); - } - if !chunk_len.is_power_of_two() { - return Err(String::from("chunk_len must be power of 2")); - } - if chunk_len == 0 { - return Err(String::from("chunk_len must be greater than 0")); - } - - let n = n2 / 2; - let k = n / chunk_len; - let mut x_ext_fft_files = Vec::new(); + /// poly and x_ext_fft should be of same length + pub fn toeplitz_part_2(&self, poly: &CtPoly, x_ext_fft: &[CtG1]) -> Vec { + let coeffs_fft = self.fft_fr(&poly.coeffs, false).unwrap(); - for offset in 0..chunk_len { - let mut x = vec![ZG1::default(); k]; - let start = if n >= chunk_len + 1 + offset { - n - chunk_len - 1 - offset - } else { - 0 - }; - let mut j = start; - for i in x.iter_mut().take(k - 1) { - i.proj = ks.secret_g1[j].proj; - if j >= chunk_len { - j -= chunk_len; - } else { - j = 0; - } - } - x[k - 1] = G1_IDENTITY; - x_ext_fft_files.push(toeplitz_part_1(&x, &ks.fs).unwrap()); + #[cfg(feature = "parallel")] + { + coeffs_fft + .into_par_iter() + .zip(x_ext_fft) + .take(poly.len()) + .map(|(coeff_fft, x_ext_fft)| x_ext_fft.mul(&coeff_fft)) + .collect() } - let new_ks = KZGSettings { - fs: ks.fs.clone(), - ..KZGSettings::default() - }; - - Ok(KzgFK20MultiSettings { - ks: new_ks, - x_ext_fft_files, - chunk_len, - length: n, //unsure if this is right - }) - } - - fn data_availability(&self, p: &PolyData) -> Result, String> { - let n = p.len(); - let n2 = n * 2; - - if n2 > self.ks.fs.max_width { - return Err(String::from( - "n2 must be equal or less than kzg settings max width", - )); + #[cfg(not(feature = "parallel"))] + { + coeffs_fft + .into_iter() + .zip(x_ext_fft) + .take(poly.len()) + .map(|(coeff_fft, x_ext_fft)| x_ext_fft.mul(&coeff_fft)) + .collect() } - if !n.is_power_of_two() { - return Err(String::from("n2 must be power of 2")); - } - - let mut out = fk20_multi_da_opt(p, self).unwrap(); - reverse_bit_order(&mut out)?; - Ok(out) } - fn data_availability_optimized(&self, p: &PolyData) -> Result, String> { - fk20_multi_da_opt(p, self) - } -} + pub fn toeplitz_part_3(&self, h_ext_fft: &[CtG1]) -> Vec { + let n2 = h_ext_fft.len(); + let n = n2 / 2; -fn fk20_single_da_opt(p: &PolyData, fk: &KzgFK20SingleSettings) -> Result, String> { - let n = p.len(); - let n2 = n * 2; + let mut ret = self.fft_g1(h_ext_fft, true).unwrap(); + ret[n..n2].copy_from_slice(&vec![CtG1::identity(); n2 - n]); - if n2 > fk.ks.fs.max_width { - return Err(String::from( - "n2 must be equal or less than kzg settings max width", - )); - } - if !n.is_power_of_two() { - return Err(String::from("n2 must be power of 2")); + ret } - - let outlen = 2 * p.len(); - let toeplitz_coeffs = toeplitz_coeffs_step(p, outlen).unwrap(); - let h_ext_fft = toeplitz_part_2(&toeplitz_coeffs, &fk.x_ext_fft, &fk.ks.fs).unwrap(); - let h = toeplitz_part_3(&h_ext_fft, &fk.ks.fs).unwrap(); - - fk.ks.fs.fft_g1(&h, false) } -fn fk20_multi_da_opt(p: &PolyData, fk: &KzgFK20MultiSettings) -> Result, String> { - let n = p.len(); - let n2 = n * 2; - - if n2 > fk.ks.fs.max_width { - return Err(String::from( - "n2 must be equal or less than kzg settings max width", - )); - } - if !n.is_power_of_two() { - return Err(String::from("n2 must be power of 2")); - } +impl CtPoly { + pub fn toeplitz_coeffs_stride(&self, offset: usize, stride: usize) -> CtPoly { + let n = self.len(); + let k = n / stride; + let k2 = k * 2; - let n = n2 / 2; - let k = n / fk.chunk_len; - let k2 = k * 2; + let mut ret = CtPoly::default(); + ret.coeffs.push(self.coeffs[n - 1 - offset]); - let mut h_ext_fft = Vec::new(); - for _i in 0..k2 { - h_ext_fft.push(G1_IDENTITY); - } - - let mut toeplitz_coeffs = PolyData::new(n2 / fk.chunk_len); - for i in 0..fk.chunk_len { - toeplitz_coeffs = - toeplitz_coeffs_stride(p, i, fk.chunk_len, toeplitz_coeffs.len()).unwrap(); - let h_ext_fft_file = - toeplitz_part_2(&toeplitz_coeffs, &fk.x_ext_fft_files[i], &fk.ks.fs).unwrap(); - for j in 0..k2 { - h_ext_fft[j] = h_ext_fft[j].add_or_dbl(&h_ext_fft_file[j]); + let num_of_zeroes = if k + 2 < k2 { k + 2 - 1 } else { k2 - 1 }; + for _ in 0..num_of_zeroes { + ret.coeffs.push(CtFr::zero()); } - } - // Calculate `h` - let mut h = toeplitz_part_3(&h_ext_fft, &fk.ks.fs).unwrap(); - - // Overwrite the second half of `h` with zero - for i in h.iter_mut().take(k2).skip(k) { - i.proj = G1_IDENTITY.proj; - } - - fk.ks.fs.fft_g1(&h, false) -} - -fn toeplitz_coeffs_step(p: &PolyData, outlen: usize) -> Result { - toeplitz_coeffs_stride(p, 0, 1, outlen) -} - -fn toeplitz_coeffs_stride( - poly: &PolyData, - offset: usize, - stride: usize, - outlen: usize, -) -> Result { - let n = poly.len(); - - if stride == 0 { - return Err(String::from("stride must be greater than 0")); - } + let mut i = k + 2; + let mut j = 2 * stride - offset - 1; + while i < k2 { + ret.coeffs.push(self.coeffs[j]); - let k = n / stride; - let k2 = k * 2; - - if outlen < k2 { - return Err(String::from("outlen must be equal or greater than k2")); - } - - let mut out = PolyData::new(outlen); - out.set_coeff_at(0, &poly.coeffs[n - 1 - offset]); - let mut i = 1; - while i <= (k + 1) && i < k2 { - out.set_coeff_at(i, &BlstFr::zero()); - i += 1; - } - let mut j = 2 * stride - offset - 1; - for i in (k + 2)..k2 { - out.set_coeff_at(i, &poly.coeffs[j]); - j += stride; - } - Ok(out) -} - -fn toeplitz_part_1(x: &[ZG1], fs: &FFTSettings) -> Result, String> { - let n = x.len(); - let n2 = n * 2; - - let mut x_ext = Vec::new(); - for i in x.iter().take(n) { - x_ext.push(*i); - } - for _i in n..n2 { - x_ext.push(G1_IDENTITY); - } - fs.fft_g1(&x_ext, false) -} - -fn toeplitz_part_2( - toeplitz_coeffs: &PolyData, - x_ext_fft: &[ZG1], - fs: &FFTSettings, -) -> Result, String> { - let toeplitz_coeffs_fft = fs.fft_fr(&toeplitz_coeffs.coeffs, false).unwrap(); - - #[cfg(feature = "parallel")] - { - let out: Vec<_> = (0..toeplitz_coeffs.len()) - .into_par_iter() - .map(|i| x_ext_fft[i].mul(&toeplitz_coeffs_fft[i])) - .collect(); - Ok(out) - } - - #[cfg(not(feature = "parallel"))] - { - let mut out = Vec::new(); - for i in 0..toeplitz_coeffs.len() { - out.push(x_ext_fft[i].mul(&toeplitz_coeffs_fft[i])); + i += 1; + j += stride; } - Ok(out) - } -} -fn toeplitz_part_3(h_ext_fft: &[ZG1], fs: &FFTSettings) -> Result, String> { - let n = h_ext_fft.len() / 2; - let mut out = fs.fft_g1(h_ext_fft, true).unwrap(); + ret + } - // Zero the second half of h - for i in out.iter_mut().take(h_ext_fft.len()).skip(n) { - i.proj = G1_IDENTITY.proj; + pub fn toeplitz_coeffs_step(&self) -> CtPoly { + self.toeplitz_coeffs_stride(0, 1) } - Ok(out) } diff --git a/constantine/src/kzg_proofs.rs b/constantine/src/kzg_proofs.rs index fd2092746..a75490e9e 100644 --- a/constantine/src/kzg_proofs.rs +++ b/constantine/src/kzg_proofs.rs @@ -1,105 +1,125 @@ -#![allow(non_camel_case_types)] -use crate::consts::{G1_GENERATOR, G2_GENERATOR}; -use crate::kzg_types::ZFr; -use crate::kzg_types::{ZFr as BlstFr, ZG1, ZG2}; -use crate::poly::PolyData; -use bls12_381::{ - multi_miller_loop, Fp12 as ZFp12, G1Affine, G2Affine, G2Prepared, MillerLoopResult, +extern crate alloc; + +#[cfg(not(feature = "parallel"))] +use alloc::vec; +use alloc::vec::Vec; +#[cfg(not(feature = "parallel"))] +use core::ptr; + +/*#[cfg(feature = "parallel")] +use blst::p1_affines; +#[cfg(not(feature = "parallel"))] +use blst::{ + blst_p1s_mult_pippenger, blst_p1s_mult_pippenger_scratch_sizeof, blst_p1s_to_affine, limb_t, }; -use kzg::eip_4844::hash_to_bls_field; -use kzg::{Fr as FrTrait, G1Mul, G2Mul}; -use std::ops::{Add, Neg}; - -#[derive(Debug, Clone)] -pub struct FFTSettings { - pub max_width: usize, - pub root_of_unity: BlstFr, - pub expanded_roots_of_unity: Vec, - pub reverse_roots_of_unity: Vec, - pub roots_of_unity: Vec, -} -pub fn expand_root_of_unity(root: &BlstFr, width: usize) -> Result, String> { - let mut generated_powers = vec![BlstFr::one(), *root]; +use blst::{ + blst_fp12_is_one, blst_p1, blst_p1_affine, blst_p1_cneg, blst_p1_to_affine, blst_p2_affine, + blst_p2_to_affine, blst_scalar, blst_scalar_from_fr, Pairing, +};*/ - while !(generated_powers.last().unwrap().is_one()) { - if generated_powers.len() > width { - return Err(String::from("Root of unity multiplied for too long")); - } +use kzg::{G1Mul, PairingVerify, G1}; - generated_powers.push(generated_powers.last().unwrap().mul(root)); - } +use crate::types::fr::CtFr; +use crate::types::g1::CtG1; +use crate::types::g2::CtG2; - if generated_powers.len() != width + 1 { - return Err(String::from("Root of unity has invalid scale")); - } +use constantine_sys::{bls12_381_g1_aff, bls12_381_g1_jac, bls12_381_g2_aff, ctt_bls12_381_g1_jac_cneg_in_place}; - Ok(generated_powers) +impl PairingVerify for CtG1 { + fn verify(a1: &CtG1, a2: &CtG2, b1: &CtG1, b2: &CtG2) -> bool { + pairings_verify(a1, a2, b1, b2) + } } -#[derive(Debug, Clone, Default)] -pub struct KZGSettings { - pub fs: FFTSettings, - pub secret_g1: Vec, - pub secret_g2: Vec, -} +pub fn g1_linear_combination(out: &mut CtG1, points: &[CtG1], scalars: &[CtFr], len: usize) { + if len < 8 { + *out = CtG1::default(); + for i in 0..len { + let tmp = points[i].mul(&scalars[i]); + *out = out.add_or_dbl(&tmp); + } + return; + } -pub fn generate_trusted_setup(len: usize, secret: [u8; 32usize]) -> (Vec, Vec) { - let s = hash_to_bls_field::(&secret); - let mut s_pow = ZFr::one(); + #[cfg(feature = "parallel")] + { + let points = unsafe { core::slice::from_raw_parts(points.as_ptr() as *const blst_p1, len) }; + let points = p1_affines::from(points); - let mut s1 = Vec::with_capacity(len); - let mut s2 = Vec::with_capacity(len); + let mut scalar_bytes: Vec = Vec::with_capacity(len * 32); + for bytes in scalars.iter().map(|b| { + let mut scalar = blst_scalar::default(); - for _ in 0..len { - s1.push(G1_GENERATOR.mul(&s_pow)); - s2.push(G2_GENERATOR.mul(&s_pow)); + unsafe { blst_scalar_from_fr(&mut scalar, &b.0) } - s_pow = s_pow.mul(&s); + scalar.b + }) { + scalar_bytes.extend_from_slice(&bytes); + } + + let res = points.mult(scalar_bytes.as_slice(), 255); + *out = CtG1(res) } - (s1, s2) -} + #[cfg(not(feature = "parallel"))] + { + let mut scratch: Vec; + unsafe { + scratch = vec![0u8; blst_p1s_mult_pippenger_scratch_sizeof(len)]; + } -pub fn eval_poly(p: &PolyData, x: &ZFr) -> ZFr { - if p.coeffs.is_empty() { - return ZFr::zero(); - } else if x.is_zero() { - return p.coeffs[0]; - } + let mut p_affine = vec![bls12_381_g1_aff::default(); len]; + let mut p_scalars = vec![blst_scalar::default(); len]; - let mut out = p.coeffs[p.coeffs.len() - 1]; - let mut i = p.coeffs.len() - 2; + let p_arg: [*const bls12_381_g1_jac; 2] = [&points[0].0, ptr::null()]; + unsafe { + blst_p1s_to_affine(p_affine.as_mut_ptr(), p_arg.as_ptr(), len); + } - loop { - let temp = out.mul(x); - out = temp.add(&p.coeffs[i]); + for i in 0..len { + unsafe { blst_scalar_from_fr(&mut p_scalars[i], &scalars[i].0) }; + } - if i == 0 { - break; + let scalars_arg: [*const blst_scalar; 2] = [p_scalars.as_ptr(), ptr::null()]; + let points_arg: [*const bls12_381_g1_aff; 2] = [p_affine.as_ptr(), ptr::null()]; + unsafe { + blst_p1s_mult_pippenger( + &mut out.0, + points_arg.as_ptr(), + len, + scalars_arg.as_ptr() as *const *const u8, + 255, + scratch.as_mut_ptr() as *mut limb_t, + ); } - i -= 1; } - out } -pub fn pairings_verify(a1: &ZG1, a2: &ZG2, b1: &ZG1, b2: &ZG2) -> bool { - let a1neg = a1.proj.neg(); +pub fn pairings_verify(a1: &CtG1, a2: &CtG2, b1: &CtG1, b2: &CtG2) -> bool { + let mut aa1 = bls12_381_g1_aff::default(); + let mut bb1 = bls12_381_g1_aff::default(); - let aa1 = G1Affine::from(&a1neg); - let bb1 = G1Affine::from(b1.proj); - let aa2 = G2Affine::from(a2.proj); - let bb2 = G2Affine::from(b2.proj); + let mut aa2 = bls12_381_g2_aff::default(); + let mut bb2 = bls12_381_g2_aff::default(); - let aa2_prepared = G2Prepared::from(aa2); - let bb2_prepared = G2Prepared::from(bb2); + // As an optimisation, we want to invert one of the pairings, + // so we negate one of the points. + let mut a1neg: CtG1 = *a1; + unsafe { + ctt_bls12_381_g1_jac_cneg_in_place(&mut a1neg.0, true); + blst_p1_to_affine(&mut aa1, &a1neg.0); - let loop0 = multi_miller_loop(&[(&aa1, &aa2_prepared)]); - let loop1 = multi_miller_loop(&[(&bb1, &bb2_prepared)]); + blst_p1_to_affine(&mut bb1, &b1.0); + blst_p2_to_affine(&mut aa2, &a2.0); + blst_p2_to_affine(&mut bb2, &b2.0); - let gt_point = loop0.add(loop1); + let dst = [0u8; 3]; + let mut pairing_blst = Pairing::new(false, &dst); + pairing_blst.raw_aggregate(&aa2, &aa1); + pairing_blst.raw_aggregate(&bb2, &bb1); + let gt_point = pairing_blst.as_fp12().final_exp(); - let new_point = MillerLoopResult::final_exponentiation(>_point); - - ZFp12::eq(&ZFp12::one(), &new_point.0) + blst_fp12_is_one(>_point) + } } diff --git a/constantine/src/kzg_types.rs b/constantine/src/kzg_types.rs deleted file mode 100644 index fbfae1193..000000000 --- a/constantine/src/kzg_types.rs +++ /dev/null @@ -1,745 +0,0 @@ -use crate::consts::{ - G1_GENERATOR, G1_IDENTITY, G1_NEGATIVE_GENERATOR, G2_GENERATOR, G2_NEGATIVE_GENERATOR, - SCALE2_ROOT_OF_UNITY, -}; -use crate::fft_g1::g1_linear_combination; -use crate::kzg_proofs::{ - expand_root_of_unity, pairings_verify, FFTSettings as ZFFTSettings, KZGSettings as ZKZGSettings, -}; -use crate::poly::PolyData; -use crate::utils::{ - blst_fr_into_pc_fr, blst_p1_into_pc_g1projective, blst_p2_into_pc_g2projective, - pc_fr_into_blst_fr, pc_g1projective_into_blst_p1, pc_g2projective_into_blst_p2, -}; -use bls12_381::{G1Affine, G1Projective, G2Affine, G2Projective, Scalar, MODULUS, R2}; -use blst::{blst_fr, blst_p1}; -use ff::Field; -use kzg::common_utils::reverse_bit_order; -use kzg::eip_4844::{BYTES_PER_FIELD_ELEMENT, BYTES_PER_G1, BYTES_PER_G2}; -use kzg::{ - FFTFr, FFTSettings, Fr as KzgFr, G1Mul, G2Mul, KZGSettings, PairingVerify, Poly, G1, G2, -}; -use std::ops::{Mul, Sub}; - -use ff::derive::sbb; -use subtle::{ConstantTimeEq, CtOption}; - -fn to_scalar(zfr: &ZFr) -> Scalar { - zfr.fr -} - -fn bigint_check_mod_256(a: &[u64; 4]) -> bool { - let (_, overflow) = a[0].overflowing_sub(MODULUS.0[0]); - let (_, overflow) = a[1].overflowing_sub(MODULUS.0[1] + overflow as u64); - let (_, overflow) = a[2].overflowing_sub(MODULUS.0[2] + overflow as u64); - let (_, overflow) = a[3].overflowing_sub(MODULUS.0[3] + overflow as u64); - overflow -} - -#[derive(Debug, Clone, Copy, Eq, PartialEq, Default)] -pub struct ZFr { - pub fr: Scalar, -} - -impl ZFr { - pub fn from_blst_fr(fr: blst_fr) -> Self { - Self { - fr: blst_fr_into_pc_fr(fr), - } - } - pub fn to_blst_fr(&self) -> blst_fr { - pc_fr_into_blst_fr(self.fr) - } - - pub fn converter(points: &[ZFr]) -> Vec { - let mut result = Vec::new(); - - for zg1 in points { - result.push(zg1.fr); - } - result - } -} - -impl KzgFr for ZFr { - fn null() -> Self { - Self { - fr: Scalar([u64::MAX, u64::MAX, u64::MAX, u64::MAX]), - } - } - fn zero() -> Self { - Self::from_u64(0) - } - - fn one() -> Self { - Self::from_u64(1) - } - - #[cfg(feature = "rand")] - fn rand() -> Self { - let rng = rand::thread_rng(); - let rusult = ff::Field::random(rng); - Self { fr: rusult } - } - #[allow(clippy::bind_instead_of_map)] - fn from_bytes(bytes: &[u8]) -> Result { - bytes - .try_into() - .map_err(|_| { - format!( - "Invalid byte length. Expected {}, got {}", - BYTES_PER_FIELD_ELEMENT, - bytes.len() - ) - }) - .and_then(|bytes: &[u8; BYTES_PER_FIELD_ELEMENT]| { - let mut tmp = Scalar([0, 0, 0, 0]); - - tmp.0[0] = u64::from_be_bytes(<[u8; 8]>::try_from(&bytes[0..8]).unwrap()); - tmp.0[1] = u64::from_be_bytes(<[u8; 8]>::try_from(&bytes[8..16]).unwrap()); - tmp.0[2] = u64::from_be_bytes(<[u8; 8]>::try_from(&bytes[16..24]).unwrap()); - tmp.0[3] = u64::from_be_bytes(<[u8; 8]>::try_from(&bytes[24..32]).unwrap()); - - // Try to subtract the modulus - let (_, borrow) = sbb(tmp.0[0], MODULUS.0[0], 0); - let (_, borrow) = sbb(tmp.0[1], MODULUS.0[1], borrow); - let (_, borrow) = sbb(tmp.0[2], MODULUS.0[2], borrow); - let (_, _borrow) = sbb(tmp.0[3], MODULUS.0[3], borrow); - let mut tmp2 = Scalar::default(); - - tmp2.0[0] = tmp.0[3]; - tmp2.0[1] = tmp.0[2]; - tmp2.0[2] = tmp.0[1]; - tmp2.0[3] = tmp.0[0]; - - let is_zero: bool = tmp2.is_zero().into(); - if !is_zero && !bigint_check_mod_256(&tmp2.0) { - return Err("Invalid scalar".to_string()); - } - - tmp2 *= &R2; - Ok(Self { fr: tmp2 }) - }) - } - fn from_bytes_unchecked(bytes: &[u8]) -> Result { - bytes - .try_into() - .map_err(|_| { - format!( - "Invalid byte length. Expected {}, got {}", - BYTES_PER_FIELD_ELEMENT, - bytes.len() - ) - }) - .map(|bytes: &[u8; BYTES_PER_FIELD_ELEMENT]| { - let mut tmp = Scalar([0, 0, 0, 0]); - - tmp.0[0] = u64::from_be_bytes(<[u8; 8]>::try_from(&bytes[0..8]).unwrap()); - tmp.0[1] = u64::from_be_bytes(<[u8; 8]>::try_from(&bytes[8..16]).unwrap()); - tmp.0[2] = u64::from_be_bytes(<[u8; 8]>::try_from(&bytes[16..24]).unwrap()); - tmp.0[3] = u64::from_be_bytes(<[u8; 8]>::try_from(&bytes[24..32]).unwrap()); - - // Try to subtract the modulus - let (_, borrow) = sbb(tmp.0[0], MODULUS.0[0], 0); - let (_, borrow) = sbb(tmp.0[1], MODULUS.0[1], borrow); - let (_, borrow) = sbb(tmp.0[2], MODULUS.0[2], borrow); - let (_, _borrow) = sbb(tmp.0[3], MODULUS.0[3], borrow); - let mut tmp2 = Scalar::default(); - - tmp2.0[0] = tmp.0[3]; - tmp2.0[1] = tmp.0[2]; - tmp2.0[2] = tmp.0[1]; - tmp2.0[3] = tmp.0[0]; - - tmp2 *= &R2; - Self { fr: tmp2 } - }) - } - - fn from_hex(hex: &str) -> Result { - let bytes = hex::decode(&hex[2..]).unwrap(); - Self::from_bytes(&bytes) - } - - fn from_u64_arr(u: &[u64; 4]) -> Self { - Self { - fr: Scalar::from_raw(*u), - } - } - - fn from_u64(val: u64) -> Self { - Self { - fr: Scalar::from(val), - } - } - - fn to_bytes(&self) -> [u8; 32] { - let scalar = self.fr; - let tmp = Scalar::montgomery_reduce( - scalar.0[0], - scalar.0[1], - scalar.0[2], - scalar.0[3], - 0, - 0, - 0, - 0, - ); - let mut res = [0; 32]; - res[0..8].copy_from_slice(&tmp.0[3].to_be_bytes()); - res[8..16].copy_from_slice(&tmp.0[2].to_be_bytes()); - res[16..24].copy_from_slice(&tmp.0[1].to_be_bytes()); - res[24..32].copy_from_slice(&tmp.0[0].to_be_bytes()); - res - } - - //testuoti - fn to_u64_arr(&self) -> [u64; 4] { - let bytes = self.to_bytes(); - [ - u64::from_be_bytes(bytes[24..32].try_into().unwrap()), - u64::from_be_bytes(bytes[16..24].try_into().unwrap()), - u64::from_be_bytes(bytes[8..16].try_into().unwrap()), - u64::from_be_bytes(bytes[0..8].try_into().unwrap()), - ] - } - - fn is_one(&self) -> bool { - self.fr.ct_eq(&ZFr::one().fr).unwrap_u8() == 1 - } - - fn is_zero(&self) -> bool { - self.fr.is_zero().unwrap_u8() == 1 - } - - fn is_null(&self) -> bool { - self.fr.ct_eq(&ZFr::null().fr).unwrap_u8() == 1 - } - - fn sqr(&self) -> Self { - Self { - fr: self.fr.square(), - } - } - - fn mul(&self, b: &Self) -> Self { - Self { - fr: Scalar::mul(&to_scalar(self), &to_scalar(b)), - } - } - - fn add(&self, b: &Self) -> Self { - Self { fr: self.fr + b.fr } - } - - fn sub(&self, b: &Self) -> Self { - Self { fr: self.fr - b.fr } - } - - fn eucl_inverse(&self) -> Self { - Self { - fr: self.fr.invert().unwrap(), - } - } - - fn negate(&self) -> Self { - Self { fr: self.fr.neg() } - } - - fn inverse(&self) -> Self { - Self { - fr: self.fr.invert().unwrap(), - } - } - - fn pow(&self, n: usize) -> Self { - let mut tmp = *self; - let mut out = Self::one(); - let mut n2 = n; - - loop { - if n2 & 1 == 1 { - out = out.mul(&tmp); - } - n2 >>= 1; - if n2 == 0 { - break; - } - tmp = tmp.sqr(); - } - - out - } - - fn div(&self, b: &Self) -> Result { - if ::is_zero(b) { - return Err("Cannot divide by zero".to_string()); - } - let tmp = b.eucl_inverse(); - let out = self.mul(&tmp); - Ok(out) - } - - fn equals(&self, b: &Self) -> bool { - self.fr == b.fr - } -} - -#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)] -pub struct ZG1 { - pub proj: G1Projective, -} - -impl ZG1 { - pub const fn from_blst_p1(p1: blst_p1) -> Self { - Self { - proj: blst_p1_into_pc_g1projective(&p1), - } - } - - pub const fn to_blst_p1(&self) -> blst_p1 { - pc_g1projective_into_blst_p1(self.proj) - } - pub const fn from_g1_projective(proj: G1Projective) -> Self { - Self { proj } - } - - fn affine_to_projective(p: G1Affine) -> Self { - Self { - proj: G1Projective::from(&p), - } - } - pub fn converter(points: &[ZG1]) -> Vec { - let mut result = Vec::new(); - - for zg1 in points { - result.push(zg1.proj); - } - result - } -} - -impl From for ZG1 { - fn from(p1: blst_p1) -> Self { - let proj = blst_p1_into_pc_g1projective(&p1); - Self { proj } - } -} - -impl G1 for ZG1 { - fn identity() -> Self { - G1_IDENTITY - } - - fn generator() -> Self { - G1_GENERATOR - } - - fn negative_generator() -> Self { - G1_NEGATIVE_GENERATOR - } - - #[cfg(feature = "rand")] - fn rand() -> Self { - let mut rng = rand::thread_rng(); - Self { - proj: G1Projective::random(&mut rng), - } - } - - #[allow(clippy::bind_instead_of_map)] - fn from_bytes(bytes: &[u8]) -> Result { - bytes - .try_into() - .map_err(|_| { - format!( - "Invalid byte length. Expected {}, got {}", - BYTES_PER_G1, - bytes.len() - ) - }) - .and_then(|bytes: &[u8; BYTES_PER_G1]| { - let affine: CtOption = G1Affine::from_compressed(bytes); - match affine.into() { - Some(x) => Ok(ZG1::affine_to_projective(x)), - None => Err("Failed to deserialize G1: Affine not available".to_string()), - } - }) - } - - fn from_hex(hex: &str) -> Result { - let bytes = hex::decode(&hex[2..]).unwrap(); - Self::from_bytes(&bytes) - } - - fn to_bytes(&self) -> [u8; 48] { - let g1_affine = G1Affine::from(self.proj); - g1_affine.to_compressed() - } - //zyme - fn add_or_dbl(&mut self, b: &Self) -> Self { - Self { - proj: self.proj + b.proj, - } - } - fn is_inf(&self) -> bool { - bool::from(self.proj.is_identity()) - } - fn is_valid(&self) -> bool { - bool::from(self.proj.is_on_curve()) - } - - fn dbl(&self) -> Self { - Self { - proj: self.proj.double(), - } - } - fn add(&self, b: &Self) -> Self { - Self { - proj: self.proj + b.proj, - } - } - - fn sub(&self, b: &Self) -> Self { - Self { - proj: self.proj.sub(&b.proj), - } - } - - fn equals(&self, b: &Self) -> bool { - self.proj.eq(&b.proj) - } -} - -impl G1Mul for ZG1 { - fn mul(&self, b: &ZFr) -> Self { - Self { - proj: self.proj.mul(b.fr), - } - } - - fn g1_lincomb(points: &[Self], scalars: &[ZFr], len: usize) -> Self { - let mut out = Self::default(); - g1_linear_combination(&mut out, points, scalars, len); - out - } -} - -impl PairingVerify for ZG1 { - fn verify(a1: &ZG1, a2: &ZG2, b1: &ZG1, b2: &ZG2) -> bool { - pairings_verify(a1, a2, b1, b2) - } -} - -#[derive(Debug, Default, PartialEq, Eq, Clone)] -pub struct ZG2 { - pub proj: G2Projective, -} - -impl ZG2 { - pub const fn from_blst_p2(p2: blst::blst_p2) -> Self { - Self { - proj: blst_p2_into_pc_g2projective(&p2), - } - } - pub const fn from_g2_projective(proj: G2Projective) -> Self { - Self { proj } - } - pub const fn to_blst_p2(&self) -> blst::blst_p2 { - pc_g2projective_into_blst_p2(self.proj) - } -} - -impl G2 for ZG2 { - fn generator() -> Self { - G2_GENERATOR - } - - fn negative_generator() -> Self { - G2_NEGATIVE_GENERATOR - } - - #[allow(clippy::bind_instead_of_map)] - fn from_bytes(bytes: &[u8]) -> Result { - bytes - .try_into() - .map_err(|_| { - format!( - "Invalid byte length. Expected {}, got {}", - BYTES_PER_G2, - bytes.len() - ) - }) - .and_then(|bytes: &[u8; BYTES_PER_G2]| { - let affine = G2Affine::from_compressed(bytes).unwrap(); - Ok(ZG2::from_g2_projective(G2Projective::from(affine))) - }) - } - - fn to_bytes(&self) -> [u8; 96] { - let g2_affine = G2Affine::from(self.proj); - g2_affine.to_compressed() - } - - fn add_or_dbl(&mut self, b: &Self) -> Self { - Self { - proj: self.proj + b.proj, - } - } - - fn dbl(&self) -> Self { - Self { - proj: self.proj.double(), - } - } - - fn sub(&self, b: &Self) -> Self { - Self { - proj: self.proj - b.proj, - } - } - - fn equals(&self, b: &Self) -> bool { - self.proj.eq(&b.proj) - } -} - -impl G2Mul for ZG2 { - fn mul(&self, b: &ZFr) -> Self { - // FIXME: Is this right? - Self { - proj: self.proj.mul(b.fr), - } - } -} - -impl Default for ZFFTSettings { - fn default() -> Self { - Self { - max_width: 0, - root_of_unity: ZFr::zero(), - expanded_roots_of_unity: Vec::new(), - reverse_roots_of_unity: Vec::new(), - roots_of_unity: Vec::new(), - } - } -} - -impl FFTSettings for ZFFTSettings { - fn new(scale: usize) -> Result { - if scale >= SCALE2_ROOT_OF_UNITY.len() { - return Err(String::from( - "Scale is expected to be within root of unity matrix row size", - )); - } - - // max_width = 2 ^ max_scale - let max_width: usize = 1 << scale; - let root_of_unity = ZFr::from_u64_arr(&SCALE2_ROOT_OF_UNITY[scale]); - - // create max_width of roots & store them reversed as well - let expanded_roots_of_unity = expand_root_of_unity(&root_of_unity, max_width).unwrap(); - let mut reverse_roots_of_unity = expanded_roots_of_unity.clone(); - reverse_roots_of_unity.reverse(); - - // Permute the roots of unity - let mut roots_of_unity = expanded_roots_of_unity.clone(); - roots_of_unity.pop(); - reverse_bit_order(&mut roots_of_unity)?; - - Ok(Self { - max_width, - root_of_unity, - expanded_roots_of_unity, - reverse_roots_of_unity, - roots_of_unity, - }) - } - - fn get_max_width(&self) -> usize { - self.max_width - } - - fn get_expanded_roots_of_unity_at(&self, i: usize) -> ZFr { - self.expanded_roots_of_unity[i] - } - - fn get_expanded_roots_of_unity(&self) -> &[ZFr] { - &self.expanded_roots_of_unity - } - - fn get_reverse_roots_of_unity_at(&self, i: usize) -> ZFr { - self.reverse_roots_of_unity[i] - } - - fn get_reversed_roots_of_unity(&self) -> &[ZFr] { - &self.reverse_roots_of_unity - } - - fn get_roots_of_unity_at(&self, i: usize) -> ZFr { - self.roots_of_unity[i] - } - - fn get_roots_of_unity(&self) -> &[ZFr] { - &self.roots_of_unity - } -} - -impl KZGSettings for ZKZGSettings { - fn new( - secret_g1: &[ZG1], - secret_g2: &[ZG2], - _length: usize, - fft_settings: &ZFFTSettings, - ) -> Result { - Ok(Self { - secret_g1: secret_g1.to_vec(), - secret_g2: secret_g2.to_vec(), - fs: fft_settings.clone(), - }) - } - - fn commit_to_poly(&self, p: &PolyData) -> Result { - if p.coeffs.len() > self.secret_g1.len() { - return Err(String::from("Polynomial is longer than secret g1")); - } - - let mut out = ZG1::default(); - g1_linear_combination(&mut out, &self.secret_g1, &p.coeffs, p.coeffs.len()); - - Ok(out) - } - - fn compute_proof_single(&self, p: &PolyData, x: &ZFr) -> Result { - if p.coeffs.is_empty() { - return Err(String::from("Polynomial must not be empty")); - } - - // `-(x0^n)`, where `n` is `1` - let divisor_0 = x.negate(); - - // Calculate `q = p / (x^n - x0^n)` for our reduced case (see `compute_proof_multi` for - // generic implementation) - let mut out_coeffs = Vec::from(&p.coeffs[1..]); - for i in (1..out_coeffs.len()).rev() { - let tmp = out_coeffs[i].mul(&divisor_0); - out_coeffs[i - 1] = out_coeffs[i - 1].sub(&tmp); - } - - let q = PolyData { coeffs: out_coeffs }; - let ret = self.commit_to_poly(&q)?; - Ok(ret) - } - - fn check_proof_single(&self, com: &ZG1, proof: &ZG1, x: &ZFr, y: &ZFr) -> Result { - let x_g2 = G2_GENERATOR.mul(x); - let s_minus_x: ZG2 = self.secret_g2[1].sub(&x_g2); - let y_g1 = G1_GENERATOR.mul(y); - let commitment_minus_y: ZG1 = com.sub(&y_g1); - - Ok(pairings_verify( - &commitment_minus_y, - &G2_GENERATOR, - proof, - &s_minus_x, - )) - } - - fn compute_proof_multi(&self, p: &PolyData, x: &ZFr, n: usize) -> Result { - if p.coeffs.is_empty() { - return Err(String::from("Polynomial must not be empty")); - } - - if !n.is_power_of_two() { - return Err(String::from("n must be a power of two")); - } - - // Construct x^n - x0^n = (x - x0.w^0)(x - x0.w^1)...(x - x0.w^(n-1)) - let mut divisor = PolyData { - coeffs: Vec::with_capacity(n + 1), - }; - - // -(x0^n) - let x_pow_n = x.pow(n); - - divisor.coeffs.push(x_pow_n.negate()); - - // Zeros - for _ in 1..n { - divisor.coeffs.push(ZFr { fr: Scalar::zero() }); - } - - // x^n - divisor.coeffs.push(ZFr { fr: Scalar::one() }); - - let mut new_polina = p.clone(); - - // Calculate q = p / (x^n - x0^n) - // let q = p.div(&divisor).unwrap(); - let q = new_polina.div(&divisor)?; - let ret = self.commit_to_poly(&q)?; - Ok(ret) - } - - fn check_proof_multi( - &self, - com: &ZG1, - proof: &ZG1, - x: &ZFr, - ys: &[ZFr], - n: usize, - ) -> Result { - if !n.is_power_of_two() { - return Err(String::from("n is not a power of two")); - } - - // Interpolate at a coset. - let mut interp = PolyData { - coeffs: self.fs.fft_fr(ys, true)?, - }; - - let inv_x = x.inverse(); // Not euclidean? - let mut inv_x_pow = inv_x; - for i in 1..n { - interp.coeffs[i] = interp.coeffs[i].mul(&inv_x_pow); - inv_x_pow = inv_x_pow.mul(&inv_x); - } - - // [x^n]_2 - let x_pow = inv_x_pow.inverse(); - - let xn2 = G2_GENERATOR.mul(&x_pow); - - // [s^n - x^n]_2 - let xn_minus_yn = self.secret_g2[n].sub(&xn2); - - // [interpolation_polynomial(s)]_1 - let is1 = self.commit_to_poly(&interp).unwrap(); - - // [commitment - interpolation_polynomial(s)]_1 = [commit]_1 - [interpolation_polynomial(s)]_1 - let commit_minus_interp = com.sub(&is1); - let ret = pairings_verify(&commit_minus_interp, &G2_GENERATOR, proof, &xn_minus_yn); - - Ok(ret) - } - - fn get_expanded_roots_of_unity_at(&self, i: usize) -> ZFr { - self.fs.get_expanded_roots_of_unity_at(i) - } - - fn get_roots_of_unity_at(&self, i: usize) -> ZFr { - self.fs.get_roots_of_unity_at(i) - } - - fn get_fft_settings(&self) -> &ZFFTSettings { - &self.fs - } - - fn get_g1_secret(&self) -> &[ZG1] { - &self.secret_g1 - } - - fn get_g2_secret(&self) -> &[ZG2] { - &self.secret_g2 - } -} diff --git a/constantine/src/lib.rs b/constantine/src/lib.rs index a59aa0b2a..b228addf7 100644 --- a/constantine/src/lib.rs +++ b/constantine/src/lib.rs @@ -1,53 +1,13 @@ -pub type Pairing = blst::Pairing; -pub type Fp = blst::blst_fp; -pub type Fp12 = blst::blst_fp12; -pub type Fp6 = blst::blst_fp6; -pub type Fr = blst::blst_fr; -pub type P1 = blst::blst_p1; -pub type P1Affine = blst::blst_p1_affine; -pub type P2 = blst::blst_p2; -pub type P2Affine = blst::blst_p2_affine; -pub type Scalar = blst::blst_scalar; -pub type Uniq = blst::blst_uniq; +#![cfg_attr(not(feature = "std"), no_std)] + pub mod consts; -pub mod das; +pub mod data_availability_sampling; pub mod eip_4844; -pub mod fft; +pub mod fft_fr; pub mod fft_g1; pub mod fk20_proofs; pub mod kzg_proofs; -pub mod kzg_types; -mod multiscalar_mul; -pub mod poly; -pub mod recover; +pub mod recovery; +pub mod types; pub mod utils; pub mod zero_poly; -trait Eq { - fn equals(&self, other: &T) -> bool; -} - -trait Inverse { - fn inverse(&self) -> T; -} - -trait Zero { - fn is_zero(&self) -> bool; -} - -impl Eq for P1 { - fn equals(&self, other: &P1) -> bool { - self.x.l.eq(&other.x.l) && self.y.l.eq(&other.x.l) && self.z.l.eq(&other.x.l) - } -} - -impl Eq for Fr { - fn equals(&self, other: &Fr) -> bool { - self.l.eq(&other.l) - } -} - -impl Zero for Fr { - fn is_zero(&self) -> bool { - self.l[0] == 0 && self.l[1] == 0 && self.l[2] == 0 && self.l[3] == 0 - } -} diff --git a/constantine/src/multiscalar_mul.rs b/constantine/src/multiscalar_mul.rs deleted file mode 100644 index 056dcd6bd..000000000 --- a/constantine/src/multiscalar_mul.rs +++ /dev/null @@ -1,194 +0,0 @@ -//! Multiscalar multiplication implementation using pippenger algorithm. -// use dusk_bytes::Serializable; - -// use alloc::vec::*; - -use crate::kzg_types::{ZFr, ZG1}; -use bls12_381::{G1Projective, Scalar}; - -pub fn divn(mut scalar: Scalar, mut n: u32) -> Scalar { - if n >= 256 { - return Scalar::from(0); - } - - while n >= 64 { - let mut t = 0; - for i in scalar.0.iter_mut().rev() { - core::mem::swap(&mut t, i); - } - n -= 64; - } - - if n > 0 { - let mut t = 0; - for i in scalar.0.iter_mut().rev() { - let t2 = *i << (64 - n); - *i >>= n; - *i |= t; - t = t2; - } - } - - scalar -} - -/// Performs a Variable Base Multiscalar Multiplication. -#[allow(clippy::needless_collect)] -pub fn msm_variable_base(points_zg1: &[ZG1], zfrscalars: &[ZFr]) -> G1Projective { - let g1_projective_vec = ZG1::converter(points_zg1); - let points = g1_projective_vec.as_slice(); - - let scalars_vec = ZFr::converter(zfrscalars); - let scalars = scalars_vec.as_slice(); - - #[cfg(feature = "parallel")] - use rayon::prelude::*; - - let c = if scalars.len() < 32 { - 3 - } else { - ln_without_floats(scalars.len()) + 2 - }; - - let num_bits = 255usize; - let fr_one = Scalar::one(); - - let zero = G1Projective::identity(); - let window_starts: Vec<_> = (0..num_bits).step_by(c).collect(); - - #[cfg(feature = "parallel")] - let window_starts_iter = window_starts.into_par_iter(); - #[cfg(not(feature = "parallel"))] - let window_starts_iter = window_starts.into_iter(); - - // Each window is of size `c`. - // We divide up the bits 0..num_bits into windows of size `c`, and - // in parallel process each such window. - let window_sums: Vec<_> = window_starts_iter - .map(|w_start| { - let mut res = zero; - // We don't need the "zero" bucket, so we only have 2^c - 1 buckets - let mut buckets = vec![zero; (1 << c) - 1]; - scalars - .iter() - .zip(points) - .filter(|(s, _)| !(*s == &Scalar::zero())) - .for_each(|(&scalar, base)| { - if scalar == fr_one { - // We only process unit scalars once in the first window. - if w_start == 0 { - res = res.add(base); - } - } else { - let mut scalar = Scalar::montgomery_reduce( - scalar.0[0], - scalar.0[1], - scalar.0[2], - scalar.0[3], - 0, - 0, - 0, - 0, - ); - - // We right-shift by w_start, thus getting rid of the - // lower bits. - scalar = divn(scalar, w_start as u32); - // We mod the remaining bits by the window size. - let scalar = scalar.0[0] % (1 << c); - - // If the scalar is non-zero, we update the corresponding - // bucket. - // (Recall that `buckets` doesn't have a zero bucket.) - if scalar != 0 { - buckets[(scalar - 1) as usize] = - buckets[(scalar - 1) as usize].add(base); - } - } - }); - - let mut running_sum = G1Projective::identity(); - for b in buckets.into_iter().rev() { - running_sum += b; - res += &running_sum; - } - - res - }) - .collect(); - - // We store the sum for the lowest window. - let lowest = *window_sums.first().unwrap(); - // We're traversing windows from high to low. - window_sums[1..] - .iter() - .rev() - .fold(zero, |mut total, sum_i| { - total += sum_i; - for _ in 0..c { - total = total.double(); - } - total - }) - + lowest -} - -fn ln_without_floats(a: usize) -> usize { - // log2(a) * ln(2) - (log2(a) * 69 / 100) as usize -} -fn log2(x: usize) -> u32 { - if x <= 1 { - return 0; - } - - let n = x.leading_zeros(); - core::mem::size_of::() as u32 * 8 - n -} - -/* - -mod tests { - #[allow(unused_imports)] - use super::*; - - #[test] - fn pippenger_test() { - // Reuse points across different tests - let mut n = 512; - let x = Scalar::from(2128506u64).invert().unwrap(); - let y = Scalar::from(4443282u64).invert().unwrap(); - let points = (0..n) - .map(|i| G1Projective::generator() * Scalar::from(1 + i as u64)) - .collect::>(); - let scalars = (0..n) - .map(|i| x + (Scalar::from(i as u64) * y)) - .collect::>(); // fast way to make ~random but deterministic scalars - let premultiplied: Vec = scalars - .iter() - .zip(points.iter()) - .map(|(sc, pt)| pt * sc) - .collect(); - while n > 0 { - let scalars = &scalars[0..n]; - let points = &points[0..n]; - let control: G1Projective = premultiplied[0..n].iter().sum(); - let subject = pippenger( - points.to_owned().into_iter(), - scalars.to_owned().into_iter(), - ); - assert_eq!(subject, control); - n = n / 2; - } - } - - #[test] - fn msm_variable_base_test() { - let points = vec![G1Affine::generator()]; - let scalars = vec![Scalar::from(100u64)]; - let premultiplied = G1Projective::generator() * Scalar::from(100u64); - let subject = msm_variable_base(&points, &scalars); - assert_eq!(subject, premultiplied); - } -} -*/ diff --git a/constantine/src/recover.rs b/constantine/src/recover.rs deleted file mode 100644 index 370f888fe..000000000 --- a/constantine/src/recover.rs +++ /dev/null @@ -1,233 +0,0 @@ -use crate::consts::SCALE_FACTOR; -use crate::kzg_proofs::FFTSettings; -use crate::kzg_types::ZFr as BlstFr; -use crate::poly::PolyData; - -use kzg::{FFTFr, Fr, Poly, PolyRecover, ZeroPoly}; - -#[cfg(feature = "parallel")] -use kzg::common_utils::next_pow_of_2; - -#[cfg(feature = "parallel")] -static mut INVERSE_FACTORS: Vec = Vec::new(); -#[cfg(feature = "parallel")] -static mut UNSCALE_FACTOR_POWERS: Vec = Vec::new(); -#[allow(clippy::needless_range_loop)] -pub fn scale_poly(p: &mut PolyData) { - let scale_factor = BlstFr::from_u64(SCALE_FACTOR); - let inv_factor = scale_factor.inverse(); - #[cfg(feature = "parallel")] - { - let optim = next_pow_of_2(p.len() - 1); - if optim <= 1024 { - unsafe { - if INVERSE_FACTORS.len() < p.len() { - if INVERSE_FACTORS.is_empty() { - INVERSE_FACTORS.push(BlstFr::one()); - } - for i in (INVERSE_FACTORS.len())..p.len() { - INVERSE_FACTORS.push(INVERSE_FACTORS[i - 1].mul(&inv_factor)); - } - } - - for i in 1..p.len() { - p.coeffs[i] = p.coeffs[i].mul(&INVERSE_FACTORS[i]); - } - } - } else { - let mut factor_power = BlstFr::one(); - for i in 1..p.len() { - factor_power = factor_power.mul(&inv_factor); - p.set_coeff_at(i, &p.get_coeff_at(i).mul(&factor_power)); - } - } - } - #[cfg(not(feature = "parallel"))] - { - let mut factor_power = BlstFr::one(); - for i in 1..p.len() { - factor_power = factor_power.mul(&inv_factor); - p.set_coeff_at(i, &p.get_coeff_at(i).mul(&factor_power)); - } - } -} - -#[allow(clippy::needless_range_loop)] -pub fn unscale_poly(p: &mut PolyData) { - let scale_factor = BlstFr::from_u64(SCALE_FACTOR); - #[cfg(feature = "parallel")] - { - unsafe { - if UNSCALE_FACTOR_POWERS.len() < p.len() { - if UNSCALE_FACTOR_POWERS.is_empty() { - UNSCALE_FACTOR_POWERS.push(BlstFr::one()); - } - for i in (UNSCALE_FACTOR_POWERS.len())..p.len() { - UNSCALE_FACTOR_POWERS.push(UNSCALE_FACTOR_POWERS[i - 1].mul(&scale_factor)); - } - } - - for i in 1..p.len() { - p.coeffs[i] = p.coeffs[i].mul(&UNSCALE_FACTOR_POWERS[i]); - } - } - } - #[cfg(not(feature = "parallel"))] - { - let mut factor_power = BlstFr::one(); - for i in 1..p.len() { - factor_power = factor_power.mul(&scale_factor); - p.set_coeff_at(i, &p.get_coeff_at(i).mul(&factor_power)); - } - } -} -impl PolyRecover for PolyData { - fn recover_poly_coeffs_from_samples( - samples: &[Option], - fs: &FFTSettings, - ) -> Result { - if !samples.len().is_power_of_two() { - return Err(String::from("samples lenght has to be power of 2")); - } - - let mut missing = Vec::new(); - - for (i, sample) in samples.iter().enumerate() { - if sample.is_none() { - missing.push(i); - } - } - - if missing.len() > samples.len() / 2 { - return Err(String::from( - "Impossible to recover, too many shards are missing", - )); - } - - // Calculate `Z_r,I` - let (zero_eval, mut zero_poly) = - fs.zero_poly_via_multiplication(samples.len(), missing.as_slice())?; - - // Check all is well - for (i, item) in zero_eval.iter().enumerate().take(samples.len()) { - if samples[i].is_none() != item.is_zero() { - return Err(String::from("sample and item are both zero")); - } - } - - // Construct E * Z_r,I: the loop makes the evaluation polynomial - - let mut poly_evaluations_with_zero = vec![BlstFr::zero(); samples.len()]; - - for i in 0..samples.len() { - if samples[i].is_none() { - poly_evaluations_with_zero[i] = BlstFr::zero(); - } else { - poly_evaluations_with_zero[i] = samples[i].unwrap().mul(&zero_eval[i]); - } - } - - // Now inverse FFT so that poly_with_zero is (E * Z_r,I)(x) = (D * Z_r,I)(x) - let mut poly_with_zero = PolyData { - coeffs: fs - .fft_fr(poly_evaluations_with_zero.as_slice(), true) - .unwrap(), - }; - - #[cfg(feature = "parallel")] - let optim = next_pow_of_2(poly_with_zero.len() - 1); - - #[cfg(feature = "parallel")] - { - if optim > 1024 { - rayon::join( - || scale_poly(&mut poly_with_zero), - || scale_poly(&mut zero_poly), - ); - } else { - scale_poly(&mut poly_with_zero); - scale_poly(&mut zero_poly); - } - } - #[cfg(not(feature = "parallel"))] - { - scale_poly(&mut poly_with_zero); - scale_poly(&mut zero_poly); - } - - // Q1 = (D * Z_r,I)(k * x) - let scaled_poly_with_zero = poly_with_zero; // Renaming - // Q2 = Z_r,I(k * x) - let scaled_zero_poly = zero_poly.coeffs; // Renaming - - let eval_scaled_poly_with_zero; - let eval_scaled_zero_poly; - - #[cfg(feature = "parallel")] - { - if optim > 1024 { - let mut eval_scaled_poly_with_zero_temp = vec![]; - let mut eval_scaled_zero_poly_temp = vec![]; - rayon::join( - || { - eval_scaled_poly_with_zero_temp = - fs.fft_fr(&scaled_poly_with_zero.coeffs, false).unwrap() - }, - || eval_scaled_zero_poly_temp = fs.fft_fr(&scaled_zero_poly, false).unwrap(), - ); - - eval_scaled_poly_with_zero = eval_scaled_poly_with_zero_temp; - eval_scaled_zero_poly = eval_scaled_zero_poly_temp; - } else { - eval_scaled_poly_with_zero = - fs.fft_fr(&scaled_poly_with_zero.coeffs, false).unwrap(); - eval_scaled_zero_poly = fs.fft_fr(&scaled_zero_poly, false).unwrap(); - } - } - #[cfg(not(feature = "parallel"))] - { - eval_scaled_poly_with_zero = fs.fft_fr(&scaled_poly_with_zero.coeffs, false).unwrap(); - eval_scaled_zero_poly = fs.fft_fr(&scaled_zero_poly, false).unwrap(); - } - - let mut eval_scaled_reconstructed_poly = eval_scaled_poly_with_zero.clone(); - for i in 0..samples.len() { - eval_scaled_reconstructed_poly[i] = eval_scaled_poly_with_zero[i] - .div(&eval_scaled_zero_poly[i]) - .unwrap(); - } - - // The result of the division is D(k * x): - let mut scaled_reconstructed_poly = PolyData { - coeffs: fs.fft_fr(&eval_scaled_reconstructed_poly, true).unwrap(), - }; - - // k * x -> x - unscale_poly(&mut scaled_reconstructed_poly); - - // Finally we have D(x) which evaluates to our original data at the powers of roots of unity - Ok(scaled_reconstructed_poly) - } - - fn recover_poly_from_samples( - samples: &[Option], - fs: &FFTSettings, - ) -> Result { - let reconstructed_poly = Self::recover_poly_coeffs_from_samples(samples, fs)?; - - // The evaluation polynomial for D(x) is the reconstructed data: - let out = PolyData { - coeffs: fs.fft_fr(&reconstructed_poly.coeffs, false).unwrap(), - }; - - // Check all is well - for (i, sample) in samples.iter().enumerate() { - if !sample.is_none() && !out.get_coeff_at(i).equals(&sample.unwrap()) { - return Err(String::from( - "sample is zero and out coeff at i is not equals to sample", - )); - } - } - Ok(out) - } -} diff --git a/constantine/src/recovery.rs b/constantine/src/recovery.rs new file mode 100644 index 000000000..978136961 --- /dev/null +++ b/constantine/src/recovery.rs @@ -0,0 +1,195 @@ +extern crate alloc; + +use alloc::string::String; +use alloc::vec::Vec; + +use kzg::{FFTFr, Fr, PolyRecover, ZeroPoly}; + +use crate::types::fft_settings::CtFFTSettings; +use crate::types::fr::CtFr; +use crate::types::poly::CtPoly; +use once_cell::sync::OnceCell; + +#[cfg(feature = "parallel")] +use rayon::prelude::*; + +const SCALE_FACTOR: u64 = 5; +static INVERSE_FACTORS: OnceCell> = OnceCell::new(); +static UNSCALE_FACTOR_POWERS: OnceCell> = OnceCell::new(); + +pub fn scale_poly(p: &mut [CtFr], len_p: usize) { + let factors = INVERSE_FACTORS.get_or_init(|| { + let scale_factor = CtFr::from_u64(SCALE_FACTOR); + let inv_factor = CtFr::inverse(&scale_factor); + let mut temp = Vec::with_capacity(65536); + temp.push(CtFr::one()); + for i in 1..65536 { + temp.push(temp[i - 1].mul(&inv_factor)); + } + temp + }); + + p.iter_mut() + .zip(factors) + .take(len_p) + .skip(1) + .for_each(|(p, factor)| { + *p = p.mul(factor); + }); +} + +pub fn unscale_poly(p: &mut [CtFr], len_p: usize) { + let factors = UNSCALE_FACTOR_POWERS.get_or_init(|| { + let scale_factor = CtFr::from_u64(SCALE_FACTOR); + let mut temp = Vec::with_capacity(65536); + temp.push(CtFr::one()); + for i in 1..65536 { + temp.push(temp[i - 1].mul(&scale_factor)); + } + temp + }); + + p.iter_mut() + .zip(factors) + .take(len_p) + .skip(1) + .for_each(|(p, factor)| { + *p = p.mul(factor); + }); +} + +impl PolyRecover for CtPoly { + fn recover_poly_coeffs_from_samples( + samples: &[Option], + fs: &CtFFTSettings, + ) -> Result { + let len_samples = samples.len(); + + if !len_samples.is_power_of_two() { + return Err(String::from( + "Samples must have a length that is a power of two", + )); + } + + let mut missing = Vec::with_capacity(len_samples / 2); + + for (i, sample) in samples.iter().enumerate() { + if sample.is_none() { + missing.push(i); + } + } + + if missing.len() > len_samples / 2 { + return Err(String::from( + "Impossible to recover, too many shards are missing", + )); + } + + // Calculate `Z_r,I` + let (zero_eval, mut zero_poly) = fs.zero_poly_via_multiplication(len_samples, &missing)?; + + // Construct E * Z_r,I: the loop makes the evaluation polynomial + let poly_evaluations_with_zero = samples + .iter() + .zip(zero_eval) + .map(|(maybe_sample, zero_eval)| { + debug_assert_eq!(maybe_sample.is_none(), zero_eval.is_zero()); + + match maybe_sample { + Some(sample) => sample.mul(&zero_eval), + None => CtFr::zero(), + } + }) + .collect::>(); + + // Now inverse FFT so that poly_with_zero is (E * Z_r,I)(x) = (D * Z_r,I)(x) + let mut poly_with_zero = fs.fft_fr(&poly_evaluations_with_zero, true).unwrap(); + drop(poly_evaluations_with_zero); + + // x -> k * x + let len_zero_poly = zero_poly.coeffs.len(); + scale_poly(&mut poly_with_zero, len_samples); + scale_poly(&mut zero_poly.coeffs, len_zero_poly); + + // Q1 = (D * Z_r,I)(k * x) + let scaled_poly_with_zero = poly_with_zero; + + // Q2 = Z_r,I(k * x) + let scaled_zero_poly = zero_poly.coeffs; + + // Polynomial division by convolution: Q3 = Q1 / Q2 + #[cfg(feature = "parallel")] + let (eval_scaled_poly_with_zero, eval_scaled_zero_poly) = { + if len_zero_poly - 1 > 1024 { + rayon::join( + || fs.fft_fr(&scaled_poly_with_zero, false).unwrap(), + || fs.fft_fr(&scaled_zero_poly, false).unwrap(), + ) + } else { + ( + fs.fft_fr(&scaled_poly_with_zero, false).unwrap(), + fs.fft_fr(&scaled_zero_poly, false).unwrap(), + ) + } + }; + #[cfg(not(feature = "parallel"))] + let (eval_scaled_poly_with_zero, eval_scaled_zero_poly) = { + ( + fs.fft_fr(&scaled_poly_with_zero, false).unwrap(), + fs.fft_fr(&scaled_zero_poly, false).unwrap(), + ) + }; + drop(scaled_zero_poly); + + let mut eval_scaled_reconstructed_poly = eval_scaled_poly_with_zero; + #[cfg(not(feature = "parallel"))] + let eval_scaled_reconstructed_poly_iter = eval_scaled_reconstructed_poly.iter_mut(); + #[cfg(feature = "parallel")] + let eval_scaled_reconstructed_poly_iter = eval_scaled_reconstructed_poly.par_iter_mut(); + + eval_scaled_reconstructed_poly_iter + .zip(eval_scaled_zero_poly) + .for_each( + |(eval_scaled_reconstructed_poly, eval_scaled_poly_with_zero)| { + *eval_scaled_reconstructed_poly = eval_scaled_reconstructed_poly + .div(&eval_scaled_poly_with_zero) + .unwrap(); + }, + ); + + // The result of the division is D(k * x): + let mut scaled_reconstructed_poly = + fs.fft_fr(&eval_scaled_reconstructed_poly, true).unwrap(); + drop(eval_scaled_reconstructed_poly); + + // k * x -> x + unscale_poly(&mut scaled_reconstructed_poly, len_samples); + + // Finally we have D(x) which evaluates to our original data at the powers of roots of unity + Ok(Self { + coeffs: scaled_reconstructed_poly, + }) + } + + fn recover_poly_from_samples( + samples: &[Option], + fs: &CtFFTSettings, + ) -> Result { + let reconstructed_poly = Self::recover_poly_coeffs_from_samples(samples, fs)?; + + // The evaluation polynomial for D(x) is the reconstructed data: + let reconstructed_data = fs.fft_fr(&reconstructed_poly.coeffs, false).unwrap(); + + // Check all is well + samples + .iter() + .zip(&reconstructed_data) + .for_each(|(sample, reconstructed_data)| { + debug_assert!(sample.is_none() || reconstructed_data.equals(&sample.unwrap())); + }); + + Ok(Self { + coeffs: reconstructed_data, + }) + } +} diff --git a/constantine/src/types/fft_settings.rs b/constantine/src/types/fft_settings.rs new file mode 100644 index 000000000..f211237b5 --- /dev/null +++ b/constantine/src/types/fft_settings.rs @@ -0,0 +1,106 @@ +extern crate alloc; + +use alloc::string::String; +use alloc::vec; +use alloc::vec::Vec; + +use kzg::common_utils::reverse_bit_order; +use kzg::{FFTSettings, Fr}; + +use crate::consts::SCALE2_ROOT_OF_UNITY; +use crate::types::fr::CtFr; + +#[derive(Debug, Clone)] +pub struct CtFFTSettings { + pub max_width: usize, + pub root_of_unity: CtFr, + pub expanded_roots_of_unity: Vec, + pub reverse_roots_of_unity: Vec, + pub roots_of_unity: Vec, +} + +impl Default for CtFFTSettings { + fn default() -> Self { + Self::new(0).unwrap() + } +} + +impl FFTSettings for CtFFTSettings { + /// Create FFTSettings with roots of unity for a selected scale. Resulting roots will have a magnitude of 2 ^ max_scale. + fn new(scale: usize) -> Result { + if scale >= SCALE2_ROOT_OF_UNITY.len() { + return Err(String::from( + "Scale is expected to be within root of unity matrix row size", + )); + } + + // max_width = 2 ^ max_scale + let max_width: usize = 1 << scale; + let root_of_unity = CtFr::from_u64_arr(&SCALE2_ROOT_OF_UNITY[scale]); + + // create max_width of roots & store them reversed as well + let expanded_roots_of_unity = expand_root_of_unity(&root_of_unity, max_width)?; + let mut reverse_roots_of_unity = expanded_roots_of_unity.clone(); + reverse_roots_of_unity.reverse(); + + // Permute the roots of unity + let mut roots_of_unity = expanded_roots_of_unity.clone(); + roots_of_unity.pop(); + reverse_bit_order(&mut roots_of_unity)?; + + Ok(CtFFTSettings { + max_width, + root_of_unity, + expanded_roots_of_unity, + reverse_roots_of_unity, + roots_of_unity, + }) + } + + fn get_max_width(&self) -> usize { + self.max_width + } + + fn get_expanded_roots_of_unity_at(&self, i: usize) -> CtFr { + self.expanded_roots_of_unity[i] + } + + fn get_expanded_roots_of_unity(&self) -> &[CtFr] { + &self.expanded_roots_of_unity + } + + fn get_reverse_roots_of_unity_at(&self, i: usize) -> CtFr { + self.reverse_roots_of_unity[i] + } + + fn get_reversed_roots_of_unity(&self) -> &[CtFr] { + &self.reverse_roots_of_unity + } + + fn get_roots_of_unity_at(&self, i: usize) -> CtFr { + self.roots_of_unity[i] + } + + fn get_roots_of_unity(&self) -> &[CtFr] { + &self.roots_of_unity + } +} + +/// Multiply a given root of unity by itself until it results in a 1 and result all multiplication values in a vector +pub fn expand_root_of_unity(root: &CtFr, width: usize) -> Result, String> { + let mut generated_powers = vec![CtFr::one(), *root]; + + while !(generated_powers.last().unwrap().is_one()) { + if generated_powers.len() > width { + return Err(String::from("Root of unity multiplied for too long")); + } + + generated_powers.push(generated_powers.last().unwrap().mul(root)); + } + + if generated_powers.len() != width + 1 { + return Err(String::from("Root of unity has invalid scale")); + } + + Ok(generated_powers) +} diff --git a/constantine/src/types/fk20_multi_settings.rs b/constantine/src/types/fk20_multi_settings.rs new file mode 100644 index 000000000..206a703d2 --- /dev/null +++ b/constantine/src/types/fk20_multi_settings.rs @@ -0,0 +1,163 @@ +extern crate alloc; + +use alloc::string::String; +use alloc::vec; +use alloc::vec::Vec; + +use kzg::common_utils::reverse_bit_order; +use kzg::{FK20MultiSettings, Poly, FFTG1, G1}; + +use crate::types::fft_settings::CtFFTSettings; +use crate::types::fr::CtFr; +use crate::types::g1::CtG1; +use crate::types::g2::CtG2; +use crate::types::kzg_settings::CtKZGSettings; +use crate::types::poly::CtPoly; + +pub struct CtFK20MultiSettings { + pub kzg_settings: CtKZGSettings, + pub chunk_len: usize, + pub x_ext_fft_files: Vec>, +} + +impl Clone for CtFK20MultiSettings { + fn clone(&self) -> Self { + Self { + kzg_settings: self.kzg_settings.clone(), + chunk_len: self.chunk_len, + x_ext_fft_files: self.x_ext_fft_files.clone(), + } + } +} + +impl Default for CtFK20MultiSettings { + fn default() -> Self { + Self { + kzg_settings: CtKZGSettings::default(), + chunk_len: 1, + x_ext_fft_files: vec![], + } + } +} + +impl FK20MultiSettings + for CtFK20MultiSettings +{ + #[allow(clippy::many_single_char_names)] + fn new(ks: &CtKZGSettings, n2: usize, chunk_len: usize) -> Result { + if n2 > ks.fs.max_width { + return Err(String::from( + "n2 must be less than or equal to kzg settings max width", + )); + } else if !n2.is_power_of_two() { + return Err(String::from("n2 must be a power of two")); + } else if n2 < 2 { + return Err(String::from("n2 must be greater than or equal to 2")); + } else if chunk_len > n2 / 2 { + return Err(String::from("chunk_len must be greater or equal to n2 / 2")); + } else if !chunk_len.is_power_of_two() { + return Err(String::from("chunk_len must be a power of two")); + } + + let n = n2 / 2; + let k = n / chunk_len; + + let mut ext_fft_files = Vec::with_capacity(chunk_len); + { + let mut x = Vec::with_capacity(k); + for offset in 0..chunk_len { + let mut start = 0; + if n >= chunk_len + 1 + offset { + start = n - chunk_len - 1 - offset; + } + + let mut i = 0; + let mut j = start; + + while i + 1 < k { + x.push(ks.secret_g1[j]); + + i += 1; + + if j >= chunk_len { + j -= chunk_len; + } else { + j = 0; + } + } + x.push(CtG1::identity()); + + let ext_fft_file = ks.fs.toeplitz_part_1(&x); + x.clear(); + ext_fft_files.push(ext_fft_file); + } + } + + let ret = Self { + kzg_settings: ks.clone(), + chunk_len, + x_ext_fft_files: ext_fft_files, + }; + + Ok(ret) + } + + fn data_availability(&self, p: &CtPoly) -> Result, String> { + let n = p.len(); + let n2 = n * 2; + + if n2 > self.kzg_settings.fs.max_width { + return Err(String::from( + "n2 must be less than or equal to kzg settings max width", + )); + } + + if !n2.is_power_of_two() { + return Err(String::from("n2 must be a power of two")); + } + + let mut ret = self.data_availability_optimized(p).unwrap(); + reverse_bit_order(&mut ret)?; + + Ok(ret) + } + + fn data_availability_optimized(&self, p: &CtPoly) -> Result, String> { + let n = p.len(); + let n2 = n * 2; + + if n2 > self.kzg_settings.fs.max_width { + return Err(String::from( + "n2 must be less than or equal to kzg settings max width", + )); + } else if !n2.is_power_of_two() { + return Err(String::from("n2 must be a power of two")); + } + + let n = n2 / 2; + let k = n / self.chunk_len; + let k2 = k * 2; + + let mut h_ext_fft = vec![CtG1::identity(); k2]; + + for i in 0..self.chunk_len { + let toeplitz_coeffs = p.toeplitz_coeffs_stride(i, self.chunk_len); + let h_ext_fft_file = self + .kzg_settings + .fs + .toeplitz_part_2(&toeplitz_coeffs, &self.x_ext_fft_files[i]); + + for j in 0..k2 { + h_ext_fft[j] = h_ext_fft[j].add_or_dbl(&h_ext_fft_file[j]); + } + } + + let mut h = self.kzg_settings.fs.toeplitz_part_3(&h_ext_fft); + + h[k..k2].copy_from_slice(&vec![CtG1::identity(); k2 - k]); + + let ret = self.kzg_settings.fs.fft_g1(&h, false).unwrap(); + + Ok(ret) + } +} diff --git a/constantine/src/types/fk20_single_settings.rs b/constantine/src/types/fk20_single_settings.rs new file mode 100644 index 000000000..12bdbd747 --- /dev/null +++ b/constantine/src/types/fk20_single_settings.rs @@ -0,0 +1,99 @@ +extern crate alloc; + +use alloc::string::String; +use alloc::vec::Vec; + +use kzg::common_utils::reverse_bit_order; +use kzg::{FK20SingleSettings, Poly, FFTG1, G1}; + +use crate::types::fft_settings::CtFFTSettings; +use crate::types::fr::CtFr; +use crate::types::g1::CtG1; +use crate::types::g2::CtG2; +use crate::types::kzg_settings::CtKZGSettings; +use crate::types::poly::CtPoly; + +#[derive(Debug, Clone, Default)] +pub struct CtFK20SingleSettings { + pub kzg_settings: CtKZGSettings, + pub x_ext_fft: Vec, +} + +impl FK20SingleSettings + for CtFK20SingleSettings +{ + fn new(kzg_settings: &CtKZGSettings, n2: usize) -> Result { + let n = n2 / 2; + + if n2 > kzg_settings.fs.max_width { + return Err(String::from( + "n2 must be less than or equal to kzg settings max width", + )); + } else if !n2.is_power_of_two() { + return Err(String::from("n2 must be a power of two")); + } else if n2 < 2 { + return Err(String::from("n2 must be greater than or equal to 2")); + } + + let mut x = Vec::with_capacity(n); + for i in 0..n - 1 { + x.push(kzg_settings.secret_g1[n - 2 - i]); + } + x.push(CtG1::identity()); + + let x_ext_fft = kzg_settings.fs.toeplitz_part_1(&x); + drop(x); + let kzg_settings = kzg_settings.clone(); + + let ret = Self { + kzg_settings, + x_ext_fft, + }; + + Ok(ret) + } + + fn data_availability(&self, p: &CtPoly) -> Result, String> { + let n = p.len(); + let n2 = n * 2; + + if n2 > self.kzg_settings.fs.max_width { + return Err(String::from( + "n2 must be less than or equal to kzg settings max width", + )); + } else if !n2.is_power_of_two() { + return Err(String::from("n2 must be a power of two")); + } + + let mut ret = self.data_availability_optimized(p).unwrap(); + reverse_bit_order(&mut ret)?; + + Ok(ret) + } + + fn data_availability_optimized(&self, p: &CtPoly) -> Result, String> { + let n = p.len(); + let n2 = n * 2; + + if n2 > self.kzg_settings.fs.max_width { + return Err(String::from( + "n2 must be less than or equal to kzg settings max width", + )); + } else if !n2.is_power_of_two() { + return Err(String::from("n2 must be a power of two")); + } + + let toeplitz_coeffs = p.toeplitz_coeffs_step(); + + let h_ext_fft = self + .kzg_settings + .fs + .toeplitz_part_2(&toeplitz_coeffs, &self.x_ext_fft); + + let h = self.kzg_settings.fs.toeplitz_part_3(&h_ext_fft); + + let ret = self.kzg_settings.fs.fft_g1(&h, false).unwrap(); + + Ok(ret) + } +} diff --git a/constantine/src/types/fr.rs b/constantine/src/types/fr.rs new file mode 100644 index 000000000..d9100df32 --- /dev/null +++ b/constantine/src/types/fr.rs @@ -0,0 +1,253 @@ +//blst_fp = bls12_381_fp, FsG1 = CtG1, blst_p1 = bls12_381_g1_jac, blst_fr = bls12_381_fr +extern crate alloc; + +use alloc::format; +use alloc::string::String; +use alloc::string::ToString; + + +use kzg::eip_4844::BYTES_PER_FIELD_ELEMENT; +use kzg::Fr; + +use constantine_sys::{bls12_381_fr, ctt_bls12_381_fr_square, ctt_bls12_381_fr_sum, ctt_bls12_381_fr_diff, ctt_bls12_381_fr_prod, ctt_bls12_381_fr_inv, + ctt_bls12_381_fr_cneg_in_place}; + +#[derive(Debug, Clone, Copy, Eq, PartialEq, Default)] +pub struct CtFr(pub bls12_381_fr); + +impl Fr for CtFr { + fn null() -> Self { + Self::from_u64_arr(&[u64::MAX, u64::MAX, u64::MAX, u64::MAX]) + } + + fn zero() -> Self { + Self::from_u64(0) + } + + fn one() -> Self { + Self::from_u64(1) + } + + #[cfg(feature = "rand")] + fn rand() -> Self { + let val: [u64; 4] = [ + rand::random(), + rand::random(), + rand::random(), + rand::random(), + ]; + let mut ret = Self::default(); + unsafe { + blst_fr_from_uint64(&mut ret.0, val.as_ptr()); + } + + ret + } + + fn from_bytes(bytes: &[u8]) -> Result { + bytes + .try_into() + .map_err(|_| { + format!( + "Invalid byte length. Expected {}, got {}", + BYTES_PER_FIELD_ELEMENT, + bytes.len() + ) + }) + .and_then(|bytes: &[u8; BYTES_PER_FIELD_ELEMENT]| { + let mut bls_scalar = blst_scalar::default(); + let mut fr = bls12_381_fr::default(); + unsafe { + blst_scalar_from_bendian(&mut bls_scalar, bytes.as_ptr()); + if !blst_scalar_fr_check(&bls_scalar) { + return Err("Invalid scalar".to_string()); + } + blst_fr_from_scalar(&mut fr, &bls_scalar); + } + Ok(Self(fr)) + }) + } + + fn from_bytes_unchecked(bytes: &[u8]) -> Result { + bytes + .try_into() + .map_err(|_| { + format!( + "Invalid byte length. Expected {}, got {}", + BYTES_PER_FIELD_ELEMENT, + bytes.len() + ) + }) + .map(|bytes: &[u8; BYTES_PER_FIELD_ELEMENT]| { + let mut bls_scalar = blst_scalar::default(); + let mut fr = bls12_381_fr::default(); + unsafe { + blst_scalar_from_bendian(&mut bls_scalar, bytes.as_ptr()); + blst_fr_from_scalar(&mut fr, &bls_scalar); + } + Self(fr) + }) + } + + fn from_hex(hex: &str) -> Result { + let bytes = hex::decode(&hex[2..]).unwrap(); + Self::from_bytes(&bytes) + } + + fn from_u64_arr(u: &[u64; 4]) -> Self { + let mut ret = Self::default(); + unsafe { + blst_fr_from_uint64(&mut ret.0, u.as_ptr()); + } + + ret + } + + fn from_u64(val: u64) -> Self { + Self::from_u64_arr(&[val, 0, 0, 0]) + } + + fn to_bytes(&self) -> [u8; 32] { + let mut scalar = blst_scalar::default(); + let mut bytes = [0u8; 32]; + unsafe { + blst_scalar_from_fr(&mut scalar, &self.0); + blst_bendian_from_scalar(bytes.as_mut_ptr(), &scalar); + } + + bytes + } + + fn to_u64_arr(&self) -> [u64; 4] { + let mut val: [u64; 4] = [0; 4]; + unsafe { + blst_uint64_from_fr(val.as_mut_ptr(), &self.0); + } + + val + } + + fn is_one(&self) -> bool { + let mut val: [u64; 4] = [0; 4]; + unsafe { + blst_uint64_from_fr(val.as_mut_ptr(), &self.0); + } + + val[0] == 1 && val[1] == 0 && val[2] == 0 && val[3] == 0 + } + + fn is_zero(&self) -> bool { + let mut val: [u64; 4] = [0; 4]; + unsafe { + blst_uint64_from_fr(val.as_mut_ptr(), &self.0); + } + + val[0] == 0 && val[1] == 0 && val[2] == 0 && val[3] == 0 + } + + fn is_null(&self) -> bool { + self.equals(&Self::null()) + } + + fn sqr(&self) -> Self { + let mut ret = Self::default(); + unsafe { + ctt_bls12_381_fr_square(&mut ret.0, &self.0) + } + + ret + } + + fn mul(&self, b: &Self) -> Self { + let mut ret = Self::default(); + unsafe { + ctt_bls12_381_fr_prod(&mut ret.0, &self.0, &b.0); + } + + ret + } + + fn add(&self, b: &Self) -> Self { + let mut ret = Self::default(); + unsafe { + ctt_bls12_381_fr_sum(&mut ret.0, &self.0, &b.0); + } + + ret + } + + fn sub(&self, b: &Self) -> Self { + let mut ret = Self::default(); + unsafe { + ctt_bls12_381_fr_diff(&mut ret.0, &self.0, &b.0); + } + + ret + } + + fn eucl_inverse(&self) -> Self { + let mut ret = Self::default(); + unsafe { + ctt_bls12_381_fr_inv(&mut ret.0, &self.0); + } + + ret + } + + fn negate(&self) -> Self { + let mut ret = Self::default(); + unsafe { + ctt_bls12_381_fr_cneg_in_place(&mut ret.0, &self.0, true); + } + + ret + } + + fn inverse(&self) -> Self { + let mut ret = Self::default(); + unsafe { + ctt_bls12_381_fr_inv(&mut ret.0, &self.0); + } + + ret + } + + fn pow(&self, n: usize) -> Self { + let mut out = Self::one(); + + let mut temp = *self; + let mut n = n; + loop { + if (n & 1) == 1 { + out = out.mul(&temp); + } + n >>= 1; + if n == 0 { + break; + } + + temp = temp.sqr(); + } + + out + } + + fn div(&self, b: &Self) -> Result { + let tmp = b.eucl_inverse(); + let out = self.mul(&tmp); + + Ok(out) + } + + fn equals(&self, b: &Self) -> bool { + let mut val_a: [u64; 4] = [0; 4]; + let mut val_b: [u64; 4] = [0; 4]; + + unsafe { + blst_uint64_from_fr(val_a.as_mut_ptr(), &self.0); + blst_uint64_from_fr(val_b.as_mut_ptr(), &b.0); + } + + val_a[0] == val_b[0] && val_a[1] == val_b[1] && val_a[2] == val_b[2] && val_a[3] == val_b[3] + } +} diff --git a/constantine/src/types/g1.rs b/constantine/src/types/g1.rs new file mode 100644 index 000000000..4f3983768 --- /dev/null +++ b/constantine/src/types/g1.rs @@ -0,0 +1,171 @@ +extern crate alloc; + +use alloc::format; +use alloc::string::String; +use alloc::string::ToString; + +use kzg::common_utils::log_2_byte; +use kzg::eip_4844::BYTES_PER_G1; +use kzg::{G1Mul, G1}; + +use crate::consts::{G1_GENERATOR, G1_IDENTITY, G1_NEGATIVE_GENERATOR}; +use crate::kzg_proofs::g1_linear_combination; +use crate::types::fr::CtFr; + +use constantine_sys::{bls12_381_fp, bls12_381_g1_jac, bls12_381_g1_aff, ctt_bls12_381_g1_jac_double, ctt_bls12_381_g1_jac_sum, ctt_bls12_381_g1_jac_is_inf, + ctt_bls12_381_g1_jac_is_eq, ctt_bls12_381_g1_jac_cneg_in_place, ctt_bls12_381_g1_jac_from_affine}; + +#[repr(C)] +#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)] +pub struct CtG1(pub bls12_381_g1_jac); + +impl CtG1 { + pub(crate) const fn from_xyz(x: bls12_381_fp, y: bls12_381_fp, z: bls12_381_fp) -> Self { + CtG1(bls12_381_g1_jac { x, y, z }) + } +} + +impl G1 for CtG1 { + fn identity() -> Self { + G1_IDENTITY + } + + fn generator() -> Self { + G1_GENERATOR + } + + fn negative_generator() -> Self { + G1_NEGATIVE_GENERATOR + } + + #[cfg(feature = "rand")] + fn rand() -> Self { + let result: CtG1 = G1_GENERATOR; + result.mul(&kzg::Fr::rand()) + } + + fn from_bytes(bytes: &[u8]) -> Result { + bytes + .try_into() + .map_err(|_| { + format!( + "Invalid byte length. Expected {}, got {}", + BYTES_PER_G1, + bytes.len() + ) + }) + .and_then(|bytes: &[u8; BYTES_PER_G1]| { + let mut tmp = bls12_381_g1_aff::default(); + let mut g1 = bls12_381_g1_jac::default(); + unsafe { + // The uncompress routine also checks that the point is on the curve + if blst_p1_uncompress(&mut tmp, bytes.as_ptr()) != BLST_ERROR::BLST_SUCCESS { + return Err("Failed to uncompress".to_string()); + } + ctt_bls12_381_g1_jac_from_affine(&mut g1, &tmp); + } + Ok(CtG1(g1)) + }) + } + + fn from_hex(hex: &str) -> Result { + let bytes = hex::decode(&hex[2..]).unwrap(); + Self::from_bytes(&bytes) + } + + fn to_bytes(&self) -> [u8; 48] { + let mut out = [0u8; BYTES_PER_G1]; + unsafe { + blst_p1_compress(out.as_mut_ptr(), &self.0); + } + out + } + + fn add_or_dbl(&mut self, b: &Self) -> Self { + let mut ret = Self::default(); + unsafe { + blst_p1_add_or_double(&mut ret.0, &self.0, &b.0); + } + ret + } + + fn is_inf(&self) -> bool { + unsafe { ctt_bls12_381_g1_jac_is_inf(&self.0) } + } + + fn is_valid(&self) -> bool { + unsafe { + // The point must be on the right subgroup + blst_p1_in_g1(&self.0) + } + } + + fn dbl(&self) -> Self { + let mut result = bls12_381_g1_jac::default(); + unsafe { + ctt_bls12_381_g1_jac_double(&mut result, &self.0); + } + Self(result) + } + + fn add(&self, b: &Self) -> Self { + let mut ret = Self::default(); + unsafe { + ctt_bls12_381_g1_jac_sum(&mut ret.0, &self.0, &b.0); + } + ret + } + + fn sub(&self, b: &Self) -> Self { + let mut b_negative: CtG1 = *b; + let mut ret = Self::default(); + unsafe { + ctt_bls12_381_g1_jac_cneg_in_place(&mut b_negative.0, true); + blst_p1_add_or_double(&mut ret.0, &self.0, &b_negative.0); + ret + } + } + + fn equals(&self, b: &Self) -> bool { + unsafe { ctt_bls12_381_g1_jac_is_eq(&self.0, &b.0) } + } +} + +impl G1Mul for CtG1 { + fn mul(&self, b: &CtFr) -> Self { + let mut scalar: blst_scalar = blst_scalar::default(); + unsafe { + blst_scalar_from_fr(&mut scalar, &b.0); + } + + // Count the number of bytes to be multiplied. + let mut i = scalar.b.len(); + while i != 0 && scalar.b[i - 1] == 0 { + i -= 1; + } + + let mut result = Self::default(); + if i == 0 { + return G1_IDENTITY; + } else if i == 1 && scalar.b[0] == 1 { + return *self; + } else { + // Count the number of bits to be multiplied. + unsafe { + blst_p1_mult( + &mut result.0, + &self.0, + &(scalar.b[0]), + 8 * i - 7 + log_2_byte(scalar.b[i - 1]), + ); + } + } + result + } + + fn g1_lincomb(points: &[Self], scalars: &[CtFr], len: usize) -> Self { + let mut out = CtG1::default(); + g1_linear_combination(&mut out, points, scalars, len); + out + } +} diff --git a/constantine/src/types/g2.rs b/constantine/src/types/g2.rs new file mode 100644 index 000000000..100914ff7 --- /dev/null +++ b/constantine/src/types/g2.rs @@ -0,0 +1,121 @@ +extern crate alloc; + +use alloc::format; +use alloc::string::String; +use alloc::string::ToString; + +use kzg::eip_4844::BYTES_PER_G2; +#[cfg(feature = "rand")] +use kzg::Fr; +use kzg::{G2Mul, G2}; + +use crate::consts::{G2_GENERATOR, G2_NEGATIVE_GENERATOR}; +use crate::types::fr::CtFr; + +use constantine_sys::{bls12_381_g2_jac, bls12_381_g2_aff, ctt_bls12_381_g2_jac_cneg_in_place, bls12_381_fp2, ctt_bls12_381_fp2_double_in_place, + ctt_bls12_381_g2_jac_from_affine, ctt_bls12_381_g1_jac_is_eq}; + +#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)] +pub struct CtG2(pub bls12_381_g2_jac); + +impl G2Mul for CtG2 { + fn mul(&self, b: &CtFr) -> Self { + let mut result = bls12_381_g2_jac::default(); + let mut scalar = blst_scalar::default(); + unsafe { + blst_scalar_from_fr(&mut scalar, &b.0); + blst_p2_mult( + &mut result, + &self.0, + scalar.b.as_ptr(), + 8 * core::mem::size_of::(), + ); + } + Self(result) + } +} + +impl G2 for CtG2 { + fn generator() -> Self { + G2_GENERATOR + } + + fn negative_generator() -> Self { + G2_NEGATIVE_GENERATOR + } + + fn from_bytes(bytes: &[u8]) -> Result { + bytes + .try_into() + .map_err(|_| { + format!( + "Invalid byte length. Expected {}, got {}", + BYTES_PER_G2, + bytes.len() + ) + }) + .and_then(|bytes: &[u8; BYTES_PER_G2]| { + let mut tmp = bls12_381_g2_aff::default(); + let mut g2 = bls12_381_g2_jac::default(); + unsafe { + // The uncompress routine also checks that the point is on the curve + if blst_p2_uncompress(&mut tmp, bytes.as_ptr()) != BLST_ERROR::BLST_SUCCESS { + return Err("Failed to uncompress".to_string()); + } + ctt_bls12_381_g2_jac_from_affine(&mut g2, &tmp); + } + Ok(CtG2(g2)) + }) + } + + fn to_bytes(&self) -> [u8; 96] { + let mut out = [0u8; BYTES_PER_G2]; + unsafe { + blst_p2_compress(out.as_mut_ptr(), &self.0); + } + out + } + + fn add_or_dbl(&mut self, b: &Self) -> Self { + let mut result = bls12_381_g2_jac::default(); + unsafe { + blst_p2_add_or_double(&mut result, &self.0, &b.0); + } + Self(result) + } + + fn dbl(&self) -> Self { + let mut result = bls12_381_g2_jac::default(); + unsafe { + ctt_bls12_381_fp2_double_in_place(&mut result); + } + Self(result) + } + + fn sub(&self, b: &Self) -> Self { + let mut bneg: bls12_381_g2_jac = b.0; + let mut result = bls12_381_g2_jac::default(); + unsafe { + //blst_p2_cneg(&mut bneg, true); + ctt_bls12_381_g2_jac_cneg_in_place(&mut bneg, true); + blst_p2_add_or_double(&mut result, &self.0, &bneg); + } + Self(result) + } + + fn equals(&self, b: &Self) -> bool { + unsafe { ctt_bls12_381_g1_jac_is_eq(&self.0, &b.0) } + } +} + +impl CtG2 { + pub(crate) fn _from_xyz(x: bls12_381_fp2, y: bls12_381_fp2, z: bls12_381_fp2) -> Self { + CtG2(bls12_381_g2_jac { x, y, z }) + } + + #[cfg(feature = "rand")] + pub fn rand() -> Self { + let result: CtG2 = G2_GENERATOR; + result.mul(&CtFr::rand()) + } +} diff --git a/constantine/src/types/kzg_settings.rs b/constantine/src/types/kzg_settings.rs new file mode 100644 index 000000000..4c64abb2f --- /dev/null +++ b/constantine/src/types/kzg_settings.rs @@ -0,0 +1,191 @@ +extern crate alloc; + +use alloc::string::String; +use alloc::vec::Vec; + +use kzg::{FFTFr, FFTSettings, Fr, G1Mul, G2Mul, KZGSettings, Poly, G1, G2}; + +use crate::consts::{G1_GENERATOR, G2_GENERATOR}; +use crate::kzg_proofs::{g1_linear_combination, pairings_verify}; +use crate::types::fft_settings::CtFFTSettings; +use crate::types::fr::CtFr; +use crate::types::g1::CtG1; +use crate::types::g2::CtG2; +use crate::types::poly::CtPoly; + +#[derive(Debug, Clone, Default)] +pub struct CtKZGSettings { + pub fs: CtFFTSettings, + pub secret_g1: Vec, + pub secret_g2: Vec, +} + +impl KZGSettings for CtKZGSettings { + fn new( + secret_g1: &[CtG1], + secret_g2: &[CtG2], + _length: usize, + fft_settings: &CtFFTSettings, + ) -> Result { + Ok(Self { + secret_g1: secret_g1.to_vec(), + secret_g2: secret_g2.to_vec(), + fs: fft_settings.clone(), + }) + } + + fn commit_to_poly(&self, poly: &CtPoly) -> Result { + if poly.coeffs.len() > self.secret_g1.len() { + return Err(String::from("Polynomial is longer than secret g1")); + } + + let mut out = CtG1::default(); + g1_linear_combination(&mut out, &self.secret_g1, &poly.coeffs, poly.coeffs.len()); + + Ok(out) + } + + fn compute_proof_single(&self, p: &CtPoly, x: &CtFr) -> Result { + if p.coeffs.is_empty() { + return Err(String::from("Polynomial must not be empty")); + } + + // `-(x0^n)`, where `n` is `1` + let divisor_0 = x.negate(); + + // Calculate `q = p / (x^n - x0^n)` for our reduced case (see `compute_proof_multi` for + // generic implementation) + let mut out_coeffs = Vec::from(&p.coeffs[1..]); + for i in (1..out_coeffs.len()).rev() { + let tmp = out_coeffs[i].mul(&divisor_0); + out_coeffs[i - 1] = out_coeffs[i - 1].sub(&tmp); + } + + let q = CtPoly { coeffs: out_coeffs }; + + let ret = self.commit_to_poly(&q)?; + + Ok(ret) + } + + fn check_proof_single( + &self, + com: &CtG1, + proof: &CtG1, + x: &CtFr, + y: &CtFr, + ) -> Result { + let x_g2: CtG2 = G2_GENERATOR.mul(x); + let s_minus_x: CtG2 = self.secret_g2[1].sub(&x_g2); + let y_g1 = G1_GENERATOR.mul(y); + let commitment_minus_y: CtG1 = com.sub(&y_g1); + + Ok(pairings_verify( + &commitment_minus_y, + &G2_GENERATOR, + proof, + &s_minus_x, + )) + } + + fn compute_proof_multi(&self, p: &CtPoly, x0: &CtFr, n: usize) -> Result { + if p.coeffs.is_empty() { + return Err(String::from("Polynomial must not be empty")); + } + + if !n.is_power_of_two() { + return Err(String::from("n must be a power of two")); + } + + // Construct x^n - x0^n = (x - x0.w^0)(x - x0.w^1)...(x - x0.w^(n-1)) + let mut divisor = CtPoly { + coeffs: Vec::with_capacity(n + 1), + }; + + // -(x0^n) + let x_pow_n = x0.pow(n); + + divisor.coeffs.push(x_pow_n.negate()); + + // Zeros + for _ in 1..n { + divisor.coeffs.push(Fr::zero()); + } + + // x^n + divisor.coeffs.push(Fr::one()); + + let mut new_polina = p.clone(); + + // Calculate q = p / (x^n - x0^n) + // let q = p.div(&divisor).unwrap(); + let q = new_polina.div(&divisor)?; + + let ret = self.commit_to_poly(&q)?; + + Ok(ret) + } + + fn check_proof_multi( + &self, + com: &CtG1, + proof: &CtG1, + x: &CtFr, + ys: &[CtFr], + n: usize, + ) -> Result { + if !n.is_power_of_two() { + return Err(String::from("n is not a power of two")); + } + + // Interpolate at a coset. + let mut interp = CtPoly { + coeffs: self.fs.fft_fr(ys, true)?, + }; + + let inv_x = x.inverse(); // Not euclidean? + let mut inv_x_pow = inv_x; + for i in 1..n { + interp.coeffs[i] = interp.coeffs[i].mul(&inv_x_pow); + inv_x_pow = inv_x_pow.mul(&inv_x); + } + + // [x^n]_2 + let x_pow = inv_x_pow.inverse(); + + let xn2 = G2_GENERATOR.mul(&x_pow); + + // [s^n - x^n]_2 + let xn_minus_yn = self.secret_g2[n].sub(&xn2); + + // [interpolation_polynomial(s)]_1 + let is1 = self.commit_to_poly(&interp).unwrap(); + + // [commitment - interpolation_polynomial(s)]_1 = [commit]_1 - [interpolation_polynomial(s)]_1 + let commit_minus_interp = com.sub(&is1); + + let ret = pairings_verify(&commit_minus_interp, &G2_GENERATOR, proof, &xn_minus_yn); + + Ok(ret) + } + + fn get_expanded_roots_of_unity_at(&self, i: usize) -> CtFr { + self.fs.get_expanded_roots_of_unity_at(i) + } + + fn get_roots_of_unity_at(&self, i: usize) -> CtFr { + self.fs.get_roots_of_unity_at(i) + } + + fn get_fft_settings(&self) -> &CtFFTSettings { + &self.fs + } + + fn get_g1_secret(&self) -> &[CtG1] { + &self.secret_g1 + } + + fn get_g2_secret(&self) -> &[CtG2] { + &self.secret_g2 + } +} diff --git a/constantine/src/types/mod.rs b/constantine/src/types/mod.rs new file mode 100644 index 000000000..97961af26 --- /dev/null +++ b/constantine/src/types/mod.rs @@ -0,0 +1,8 @@ +pub mod fft_settings; +pub mod fk20_multi_settings; +pub mod fk20_single_settings; +pub mod fr; +pub mod g1; +pub mod g2; +pub mod kzg_settings; +pub mod poly; diff --git a/constantine/src/poly.rs b/constantine/src/types/poly.rs similarity index 85% rename from constantine/src/poly.rs rename to constantine/src/types/poly.rs index 07851320e..f248a7b2f 100644 --- a/constantine/src/poly.rs +++ b/constantine/src/types/poly.rs @@ -4,32 +4,34 @@ use alloc::string::String; use alloc::vec; use alloc::vec::Vec; -use crate::consts::SCALE_FACTOR; -use crate::kzg_proofs::FFTSettings as ZFFTSettings; -use crate::kzg_types::ZFr; use kzg::common_utils::{log2_pow2, log2_u64, next_pow_of_2}; use kzg::{FFTFr, FFTSettings, FFTSettingsPoly, Fr, Poly}; +use crate::consts::SCALE_FACTOR; +use crate::types::fft_settings::CtFFTSettings; +use crate::types::fr::CtFr; + #[derive(Debug, Clone, Eq, PartialEq, Default)] -pub struct PolyData { - pub coeffs: Vec, +pub struct CtPoly { + pub coeffs: Vec, } -impl Poly for PolyData { + +impl Poly for CtPoly { fn new(size: usize) -> Self { Self { - coeffs: vec![ZFr::default(); size], + coeffs: vec![CtFr::default(); size], } } - fn get_coeff_at(&self, i: usize) -> ZFr { + fn get_coeff_at(&self, i: usize) -> CtFr { self.coeffs[i] } - fn set_coeff_at(&mut self, i: usize, x: &ZFr) { + fn set_coeff_at(&mut self, i: usize, x: &CtFr) { self.coeffs[i] = *x } - fn get_coeffs(&self) -> &[ZFr] { + fn get_coeffs(&self) -> &[CtFr] { &self.coeffs } @@ -37,9 +39,9 @@ impl Poly for PolyData { self.coeffs.len() } - fn eval(&self, x: &ZFr) -> ZFr { + fn eval(&self, x: &CtFr) -> CtFr { if self.coeffs.is_empty() { - return ZFr::zero(); + return CtFr::zero(); } else if x.is_zero() { return self.coeffs[0]; } @@ -60,10 +62,10 @@ impl Poly for PolyData { } fn scale(&mut self) { - let scale_factor = ZFr::from_u64(SCALE_FACTOR); + let scale_factor = CtFr::from_u64(SCALE_FACTOR); let inv_factor = scale_factor.inverse(); - let mut factor_power = ZFr::one(); + let mut factor_power = CtFr::one(); for i in 0..self.coeffs.len() { factor_power = factor_power.mul(&inv_factor); self.coeffs[i] = self.coeffs[i].mul(&factor_power); @@ -71,9 +73,9 @@ impl Poly for PolyData { } fn unscale(&mut self) { - let scale_factor = ZFr::from_u64(SCALE_FACTOR); + let scale_factor = CtFr::from_u64(SCALE_FACTOR); - let mut factor_power = ZFr::one(); + let mut factor_power = CtFr::one(); for i in 0..self.coeffs.len() { factor_power = factor_power.mul(&scale_factor); self.coeffs[i] = self.coeffs[i].mul(&factor_power); @@ -92,8 +94,8 @@ impl Poly for PolyData { )); } - let mut ret = PolyData { - coeffs: vec![ZFr::zero(); output_len], + let mut ret = CtPoly { + coeffs: vec![CtFr::zero(); output_len], }; // If the input polynomial is constant, the remainder of the series is zero if self.coeffs.len() == 1 { @@ -164,7 +166,7 @@ impl Poly for PolyData { let out_length = self.poly_quotient_length(divisor); if out_length == 0 { - return Ok(PolyData { coeffs: vec![] }); + return Ok(CtPoly { coeffs: vec![] }); } // Special case for divisor.len() == 2 @@ -182,10 +184,10 @@ impl Poly for PolyData { out_coeffs[0] = out_coeffs[0].div(&divisor_1).unwrap(); - Ok(PolyData { coeffs: out_coeffs }) + Ok(CtPoly { coeffs: out_coeffs }) } else { - let mut out: PolyData = PolyData { - coeffs: vec![ZFr::default(); out_length], + let mut out: CtPoly = CtPoly { + coeffs: vec![CtFr::default(); out_length], }; let mut a_pos = self.len() - 1; @@ -223,13 +225,13 @@ impl Poly for PolyData { // If the divisor is larger than the dividend, the result is zero-length if n > m { - return Ok(PolyData { coeffs: Vec::new() }); + return Ok(CtPoly { coeffs: Vec::new() }); } // Special case for divisor.length == 1 (it's a constant) if divisor.len() == 1 { - let mut out = PolyData { - coeffs: vec![ZFr::zero(); self.len()], + let mut out = CtPoly { + coeffs: vec![CtFr::zero(); self.len()], }; for i in 0..out.len() { out.coeffs[i] = self.coeffs[i].div(&divisor.coeffs[0]).unwrap(); @@ -249,13 +251,13 @@ impl Poly for PolyData { fn mul_direct(&mut self, multiplier: &Self, output_len: usize) -> Result { if self.len() == 0 || multiplier.len() == 0 { - return Ok(PolyData::new(0)); + return Ok(CtPoly::new(0)); } let a_degree = self.len() - 1; let b_degree = multiplier.len() - 1; - let mut ret = PolyData { + let mut ret = CtPoly { coeffs: vec![Fr::zero(); output_len], }; @@ -275,18 +277,18 @@ impl Poly for PolyData { } } -impl FFTSettingsPoly for ZFFTSettings { +impl FFTSettingsPoly for CtFFTSettings { fn poly_mul_fft( - a: &PolyData, - b: &PolyData, + a: &CtPoly, + b: &CtPoly, len: usize, - _fs: Option<&ZFFTSettings>, - ) -> Result { + _fs: Option<&CtFFTSettings>, + ) -> Result { b.mul_fft(a, len) } } -impl PolyData { +impl CtPoly { pub fn _poly_norm(&self) -> Self { let mut ret = self.clone(); @@ -314,7 +316,7 @@ impl PolyData { pub fn pad(&self, out_length: usize) -> Self { let mut ret = Self { - coeffs: vec![ZFr::zero(); out_length], + coeffs: vec![CtFr::zero(); out_length], }; for i in 0..self.len().min(out_length) { @@ -324,9 +326,9 @@ impl PolyData { ret } - pub fn flip(&self) -> Result { - let mut ret = PolyData { - coeffs: vec![ZFr::default(); self.len()], + pub fn flip(&self) -> Result { + let mut ret = CtPoly { + coeffs: vec![CtFr::default(); self.len()], }; for i in 0..self.len() { ret.coeffs[i] = self.coeffs[self.coeffs.len() - i - 1] @@ -339,13 +341,13 @@ impl PolyData { let length = next_pow_of_2(self.len() + multiplier.len() - 1); let scale = log2_pow2(length); - let fft_settings = ZFFTSettings::new(scale).unwrap(); + let fft_settings = CtFFTSettings::new(scale).unwrap(); let a_pad = self.pad(length); let b_pad = multiplier.pad(length); - let a_fft: Vec; - let b_fft: Vec; + let a_fft: Vec; + let b_fft: Vec; #[cfg(feature = "parallel")] { @@ -383,8 +385,8 @@ impl PolyData { let ab = fft_settings.fft_fr(&ab_fft, true).unwrap(); drop(ab_fft); - let mut ret = PolyData { - coeffs: vec![ZFr::zero(); output_len], + let mut ret = CtPoly { + coeffs: vec![CtFr::zero(); output_len], }; let range = ..output_len.min(length); diff --git a/constantine/src/utils.rs b/constantine/src/utils.rs index f76d95fd2..3ffa62ec3 100644 --- a/constantine/src/utils.rs +++ b/constantine/src/utils.rs @@ -1,58 +1,27 @@ -use super::P1; -use crate::P2; -use bls12_381::{Fp as ZFp, Fp2 as ZFp2, G1Projective, G2Projective, Scalar}; -use blst::{blst_fp, blst_fp2, blst_fr, blst_p1, blst_p2}; +extern crate alloc; -#[derive(Debug, PartialEq, Eq)] -pub struct Error; +use alloc::vec::Vec; -pub const fn blst_fr_into_pc_fr(fr: blst_fr) -> Scalar { - Scalar(fr.l) -} -pub const fn pc_fr_into_blst_fr(scalar: Scalar) -> blst_fr { - blst_fr { l: scalar.0 } -} -pub const fn blst_fp2_into_pc_fq2(fp: &blst_fp2) -> ZFp2 { - let c0 = ZFp(fp.fp[0].l); - let c1 = ZFp(fp.fp[1].l); - ZFp2 { c0, c1 } -} - -pub const fn blst_p1_into_pc_g1projective(p1: &P1) -> G1Projective { - let x = ZFp(p1.x.l); - let y = ZFp(p1.y.l); - let z = ZFp(p1.z.l); - G1Projective { x, y, z } -} +use kzg::eip_4844::hash_to_bls_field; +use kzg::{Fr, G1Mul, G2Mul}; -pub const fn pc_g1projective_into_blst_p1(p1: G1Projective) -> blst_p1 { - let x = blst_fp { l: p1.x.0 }; - let y = blst_fp { l: p1.y.0 }; - let z = blst_fp { l: p1.z.0 }; +use crate::consts::{G1_GENERATOR, G2_GENERATOR}; +use crate::types::g1::CtG1; +use crate::types::g2::CtG2; - blst_p1 { x, y, z } -} +pub fn generate_trusted_setup(n: usize, secret: [u8; 32usize]) -> (Vec, Vec) { + let s = hash_to_bls_field(&secret); + let mut s_pow = Fr::one(); -pub const fn blst_p2_into_pc_g2projective(p2: &P2) -> G2Projective { - G2Projective { - x: blst_fp2_into_pc_fq2(&p2.x), - y: blst_fp2_into_pc_fq2(&p2.y), - z: blst_fp2_into_pc_fq2(&p2.z), - } -} + let mut s1 = Vec::with_capacity(n); + let mut s2 = Vec::with_capacity(n); -pub const fn pc_g2projective_into_blst_p2(p2: G2Projective) -> blst_p2 { - let x = blst_fp2 { - fp: [blst_fp { l: p2.x.c0.0 }, blst_fp { l: p2.x.c1.0 }], - }; + for _ in 0..n { + s1.push(G1_GENERATOR.mul(&s_pow)); + s2.push(G2_GENERATOR.mul(&s_pow)); - let y = blst_fp2 { - fp: [blst_fp { l: p2.y.c0.0 }, blst_fp { l: p2.y.c1.0 }], - }; - - let z = blst_fp2 { - fp: [blst_fp { l: p2.z.c0.0 }, blst_fp { l: p2.z.c1.0 }], - }; + s_pow = s_pow.mul(&s); + } - blst_p2 { x, y, z } + (s1, s2) } diff --git a/constantine/src/zero_poly.rs b/constantine/src/zero_poly.rs index fc6e76f64..713058e8f 100644 --- a/constantine/src/zero_poly.rs +++ b/constantine/src/zero_poly.rs @@ -1,197 +1,314 @@ -use super::kzg_proofs::FFTSettings; -use crate::kzg_types::ZFr as BlstFr; -use crate::poly::PolyData; - -use kzg::common_utils::next_pow_of_2; -use kzg::{FFTFr, Fr, ZeroPoly}; -use std::cmp::{min, Ordering}; - -pub(crate) fn pad_poly(poly: &PolyData, new_length: usize) -> Result, String> { - if new_length <= poly.coeffs.len() { - return Ok(poly.coeffs.clone()); +extern crate alloc; + +use alloc::string::String; +use alloc::vec; +use alloc::vec::Vec; +use core::cmp::{min, Ordering}; + +use kzg::{common_utils::next_pow_of_2, FFTFr, Fr, ZeroPoly}; + +use crate::types::fft_settings::CtFFTSettings; +use crate::types::fr::CtFr; +use crate::types::poly::CtPoly; + +#[cfg(feature = "parallel")] +use rayon::prelude::*; +use smallvec::{smallvec, SmallVec}; + +// Can be tuned & optimized (must be a power of 2) +const DEGREE_OF_PARTIAL: usize = 256; +// Can be tuned & optimized (but must be a power of 2) +const REDUCTION_FACTOR: usize = 4; + +/// Pad given poly it with zeros to new length +pub fn pad_poly(mut poly: Vec, new_length: usize) -> Result, String> { + if new_length < poly.len() { + return Err(String::from( + "new_length must be longer or equal to poly length", + )); } - let mut out = poly.coeffs.to_vec(); + poly.resize(new_length, CtFr::zero()); - for _i in poly.coeffs.len()..new_length { - out.push(BlstFr::zero()) + Ok(poly) +} + +/// Pad given poly coefficients it with zeros to new length +pub fn pad_poly_coeffs( + mut coeffs: SmallVec<[T; N]>, + new_length: usize, +) -> Result, String> +where + T: Default + Clone, +{ + if new_length < coeffs.len() { + return Err(String::from( + "new_length must be longer or equal to coeffs length", + )); } - Ok(out) + coeffs.resize(new_length, T::default()); + + Ok(coeffs) } -impl ZeroPoly for FFTSettings { - #[allow(clippy::needless_range_loop)] + +impl CtFFTSettings { fn do_zero_poly_mul_partial( &self, - indices: &[usize], + idxs: &[usize], stride: usize, - ) -> Result { - if indices.is_empty() { - // == 0 - return Err(String::from("index array length mustnt be zero")); + ) -> Result, String> { + if idxs.is_empty() { + return Err(String::from("idx array must not be empty")); } - let mut poly = PolyData { - coeffs: vec![BlstFr::one(); indices.len() + 1], - }; - poly.coeffs[0] = (self.expanded_roots_of_unity[indices[0] * stride]).negate(); - - for i in 1..indices.len() { - let neg_di = (self.expanded_roots_of_unity[indices[i] * stride]).negate(); - poly.coeffs[i] = neg_di; - - poly.coeffs[i] = poly.coeffs[i].add(&poly.coeffs[i - 1]); - - let mut j = i - 1; - while j > 0 { - poly.coeffs[j] = poly.coeffs[j].mul(&neg_di); - poly.coeffs[j] = poly.coeffs[j].add(&poly.coeffs[j - 1]); - j -= 1; + + // Makes use of long multiplication in terms of (x - w_0)(x - w_1).. + let mut coeffs = SmallVec::<[CtFr; DEGREE_OF_PARTIAL]>::new(); + + // For the first member, store -w_0 as constant term + coeffs.push(self.expanded_roots_of_unity[idxs[0] * stride].negate()); + + for (i, idx) in idxs.iter().copied().enumerate().skip(1) { + // For member (x - w_i) take coefficient as -(w_i + w_{i-1} + ...) + let neg_di = self.expanded_roots_of_unity[idx * stride].negate(); + coeffs.push(neg_di.add(&coeffs[i - 1])); + + // Multiply all previous members by (x - w_i) + // It equals multiplying by - w_i and adding x^(i - 1) coefficient (implied multiplication by x) + for j in (1..i).rev() { + coeffs[j] = coeffs[j].mul(&neg_di).add(&coeffs[j - 1]); } - poly.coeffs[0] = poly.coeffs[0].mul(&neg_di); + // Multiply x^0 member by - w_i + coeffs[0] = coeffs[0].mul(&neg_di); } - Ok(poly) + coeffs.resize(idxs.len() + 1, CtFr::one()); + + Ok(coeffs) } - #[allow(clippy::needless_range_loop)] - fn reduce_partials(&self, len_out: usize, partials: &[PolyData]) -> Result { - let mut out_degree: usize = 0; - for partial in partials { - out_degree += partial.coeffs.len() - 1; - } - if out_degree + 1 > len_out { + fn reduce_partials( + &self, + domain_size: usize, + partial_coeffs: SmallVec<[SmallVec<[CtFr; DEGREE_OF_PARTIAL]>; REDUCTION_FACTOR]>, + ) -> Result, String> { + if !domain_size.is_power_of_two() { return Err(String::from("Expected domain size to be a power of 2")); } - let mut p_partial = pad_poly(&partials[0], len_out).unwrap(); - let mut mul_eval_ps = self.fft_fr(&p_partial, false).unwrap(); + if partial_coeffs.is_empty() { + return Err(String::from("partials must not be empty")); + } - for partial in partials.iter().skip(1) { - p_partial = pad_poly(partial, len_out)?; + // Calculate the resulting polynomial degree + // E.g. (a * x^n + ...) (b * x^m + ...) has a degree of x^(n+m) + let out_degree = partial_coeffs + .iter() + .map(|partial| { + // TODO: Not guaranteed by function signature that this doesn't underflow + partial.len() - 1 + }) + .sum::(); + + if out_degree + 1 > domain_size { + return Err(String::from( + "Out degree is longer than possible polynomial size in domain", + )); + } - let p_eval = self.fft_fr(&p_partial, false).unwrap(); - for j in 0..len_out { - mul_eval_ps[j].fr *= p_eval[j].fr; - } + let mut partial_coeffs = partial_coeffs.into_iter(); + + // Pad all partial polynomials to same length, compute their FFT and multiply them together + let mut padded_partial = pad_poly_coeffs( + partial_coeffs + .next() + .expect("Not empty, checked above; qed"), + domain_size, + )?; + let mut eval_result: SmallVec<[CtFr; DEGREE_OF_PARTIAL]> = + smallvec![CtFr::zero(); domain_size]; + self.fft_fr_output(&padded_partial, false, &mut eval_result)?; + + for partial in partial_coeffs { + padded_partial = pad_poly_coeffs(partial, domain_size)?; + let mut evaluated_partial: SmallVec<[CtFr; DEGREE_OF_PARTIAL]> = + smallvec![CtFr::zero(); domain_size]; + self.fft_fr_output(&padded_partial, false, &mut evaluated_partial)?; + + eval_result + .iter_mut() + .zip(evaluated_partial.iter()) + .for_each(|(eval_result, evaluated_partial)| { + *eval_result = eval_result.mul(evaluated_partial); + }); } - let coeffs = self.fft_fr(&mul_eval_ps, true)?; + let mut coeffs = smallvec![CtFr::zero(); domain_size]; + // Apply an inverse FFT to produce a new poly. Limit its size to out_degree + 1 + self.fft_fr_output(&eval_result, true, &mut coeffs)?; + coeffs.truncate(out_degree + 1); + + Ok(coeffs) + } +} - let out = PolyData { - coeffs: coeffs[..(out_degree + 1)].to_vec(), - }; +impl ZeroPoly for CtFFTSettings { + fn do_zero_poly_mul_partial(&self, idxs: &[usize], stride: usize) -> Result { + self.do_zero_poly_mul_partial(idxs, stride) + .map(|coeffs| CtPoly { + coeffs: coeffs.into_vec(), + }) + } - Ok(out) + fn reduce_partials(&self, domain_size: usize, partials: &[CtPoly]) -> Result { + self.reduce_partials( + domain_size, + partials + .iter() + .map(|partial| SmallVec::from_slice(&partial.coeffs)) + .collect(), + ) + .map(|coeffs| CtPoly { + coeffs: coeffs.into_vec(), + }) } - #[allow(clippy::comparison_chain)] + fn zero_poly_via_multiplication( &self, - length: usize, - missing_indices: &[usize], - ) -> Result<(Vec, PolyData), String> { - let zero_eval: Vec; - let mut zero_poly: PolyData; + domain_size: usize, + missing_idxs: &[usize], + ) -> Result<(Vec, CtPoly), String> { + let zero_eval: Vec; + let mut zero_poly: CtPoly; - if missing_indices.is_empty() { + if missing_idxs.is_empty() { zero_eval = Vec::new(); - zero_poly = PolyData { coeffs: Vec::new() }; + zero_poly = CtPoly { coeffs: Vec::new() }; return Ok((zero_eval, zero_poly)); } - if missing_indices.len() >= length { + if missing_idxs.len() >= domain_size { return Err(String::from("Missing idxs greater than domain size")); - } else if length > self.max_width { + } else if domain_size > self.max_width { return Err(String::from( "Domain size greater than fft_settings.max_width", )); - } else if !length.is_power_of_two() { + } else if !domain_size.is_power_of_two() { return Err(String::from("Domain size must be a power of 2")); } - let degree_of_partial = 256; - let missing_per_partial = degree_of_partial - 1; - let domain_stride = self.max_width / length; - let mut partial_count = - (missing_per_partial + missing_indices.len() - 1) / missing_per_partial; - let domain_ceiling = min(next_pow_of_2(partial_count * degree_of_partial), length); - - if missing_indices.len() <= missing_per_partial { - zero_poly = self.do_zero_poly_mul_partial(missing_indices, domain_stride)?; + let missing_per_partial = DEGREE_OF_PARTIAL - 1; // Number of missing idxs needed per partial + let domain_stride = self.max_width / domain_size; + + let mut partial_count = 1 + (missing_idxs.len() - 1) / missing_per_partial; // TODO: explain why -1 is used here + + let next_pow: usize = next_pow_of_2(partial_count * DEGREE_OF_PARTIAL); + let domain_ceiling = min(next_pow, domain_size); + // Calculate zero poly + if missing_idxs.len() <= missing_per_partial { + // When all idxs fit into a single multiplication + zero_poly = CtPoly { + coeffs: self + .do_zero_poly_mul_partial(missing_idxs, domain_stride)? + .into_vec(), + }; } else { - let mut work = vec![BlstFr::zero(); next_pow_of_2(partial_count * degree_of_partial)]; - - let mut partial_lens = Vec::new(); - - let mut offset = 0; - let mut out_offset = 0; - let max = missing_indices.len(); - - for _i in 0..partial_count { - let end = min(offset + missing_per_partial, max); - - let mut partial = - self.do_zero_poly_mul_partial(&missing_indices[offset..end], domain_stride)?; - partial.coeffs = pad_poly(&partial, degree_of_partial)?; - work.splice( - out_offset..(out_offset + degree_of_partial), - partial.coeffs.to_vec(), + // Otherwise, construct a set of partial polynomials + // Save all constructed polynomials in a shared 'work' vector + let mut work = vec![CtFr::zero(); next_pow]; + + let mut partial_lens = vec![DEGREE_OF_PARTIAL; partial_count]; + + #[cfg(not(feature = "parallel"))] + let iter = missing_idxs + .chunks(missing_per_partial) + .zip(work.chunks_exact_mut(DEGREE_OF_PARTIAL)); + #[cfg(feature = "parallel")] + let iter = missing_idxs + .par_chunks(missing_per_partial) + .zip(work.par_chunks_exact_mut(DEGREE_OF_PARTIAL)); + // Insert all generated partial polynomials at degree_of_partial intervals in work vector + iter.for_each(|(missing_idxs, work)| { + let partial_coeffs = self + .do_zero_poly_mul_partial(missing_idxs, domain_stride) + .expect("`missing_idxs` is guaranteed to not be empty; qed"); + + let partial_coeffs = pad_poly_coeffs(partial_coeffs, DEGREE_OF_PARTIAL).expect( + "`partial.coeffs.len()` (same as `missing_idxs.len() + 1`) is \ + guaranteed to be at most `degree_of_partial`; qed", ); - partial_lens.push(degree_of_partial); - - offset += missing_per_partial; - out_offset += degree_of_partial; - } + work[..DEGREE_OF_PARTIAL].copy_from_slice(&partial_coeffs); + }); + // Adjust last length to match its actual length partial_lens[partial_count - 1] = - 1 + missing_indices.len() - (partial_count - 1) * missing_per_partial; + 1 + missing_idxs.len() - (partial_count - 1) * missing_per_partial; - let reduction_factor = 4; + // Reduce all vectors into one by reducing them w/ varying size multiplications while partial_count > 1 { - let reduced_count = 1 + (partial_count - 1) / reduction_factor; + let reduced_count = 1 + (partial_count - 1) / REDUCTION_FACTOR; let partial_size = next_pow_of_2(partial_lens[0]); + // Step over polynomial space and produce larger multiplied polynomials for i in 0..reduced_count { - let start = i * reduction_factor; - let out_end = min((start + reduction_factor) * partial_size, domain_ceiling); - let reduced_len = min(out_end - start * partial_size, length); - let partials_num = min(reduction_factor, partial_count - start); - - let mut partial_vec = Vec::new(); - for j in 0..partials_num { - let k = (start + j) * partial_size; - partial_vec.push(PolyData { - coeffs: work[k..(k + partial_lens[i + j])].to_vec(), - }); + let start = i * REDUCTION_FACTOR; + let out_end = min((start + REDUCTION_FACTOR) * partial_size, domain_ceiling); + let reduced_len = min(out_end - start * partial_size, domain_size); + let partials_num = min(REDUCTION_FACTOR, partial_count - start); + + // Calculate partial views from lens and offsets + // Also update offsets to match current iteration + let partial_offset = start * partial_size; + let mut partial_coeffs = SmallVec::new(); + for (partial_offset, partial_len) in (partial_offset..) + .step_by(partial_size) + .zip(partial_lens.iter().skip(i).copied()) + .take(partials_num) + { + // We know the capacity required in `reduce_partials()` call below to avoid + // re-allocation + let mut coeffs = SmallVec::with_capacity(reduced_len); + coeffs.extend_from_slice(&work[partial_offset..][..partial_len]); + partial_coeffs.push(coeffs); } if partials_num > 1 { - let mut reduced_poly = self.reduce_partials(reduced_len, &partial_vec)?; - partial_lens[i] = reduced_poly.coeffs.len(); - reduced_poly.coeffs = pad_poly(&reduced_poly, partial_size * partials_num)?; - work.splice( - (start * partial_size) - ..(start * partial_size + reduced_poly.coeffs.len()), - reduced_poly.coeffs, - ); + let mut reduced_coeffs = + self.reduce_partials(reduced_len, partial_coeffs)?; + // Update partial length to match its length after reduction + partial_lens[i] = reduced_coeffs.len(); + reduced_coeffs = + pad_poly_coeffs(reduced_coeffs, partial_size * partials_num)?; + work[partial_offset..][..reduced_coeffs.len()] + .copy_from_slice(&reduced_coeffs); } else { + // Instead of keeping track of remaining polynomials, reuse i'th partial for start'th one partial_lens[i] = partial_lens[start]; } } + // Number of steps done equals the number of polynomials that we still need to reduce together partial_count = reduced_count; } - zero_poly = PolyData { coeffs: work }; + zero_poly = CtPoly { coeffs: work }; } - match zero_poly.coeffs.len().cmp(&length) { - Ordering::Less => zero_poly.coeffs = pad_poly(&zero_poly, length)?, - Ordering::Greater => zero_poly.coeffs = zero_poly.coeffs[..length].to_vec(), + // Pad resulting poly to expected + match zero_poly.coeffs.len().cmp(&domain_size) { + Ordering::Less => { + zero_poly.coeffs = pad_poly(zero_poly.coeffs, domain_size)?; + } Ordering::Equal => {} + Ordering::Greater => { + zero_poly.coeffs.truncate(domain_size); + } } + // Evaluate calculated poly zero_eval = self.fft_fr(&zero_poly.coeffs, false)?; + Ok((zero_eval, zero_poly)) } } diff --git a/constantine/tests/bls12_381.rs b/constantine/tests/bls12_381.rs index a123f6628..ca5be831e 100644 --- a/constantine/tests/bls12_381.rs +++ b/constantine/tests/bls12_381.rs @@ -1,114 +1,121 @@ #[cfg(test)] mod tests { use kzg::common_utils::log_2_byte; - use kzg_bench::tests::bls12_381::*; - use rust_kzg_zkcrypto::fft_g1::g1_linear_combination; - use rust_kzg_zkcrypto::kzg_proofs::pairings_verify; - use rust_kzg_zkcrypto::kzg_types::{ZFr, ZG1, ZG2}; + use kzg_bench::tests::bls12_381::{ + fr_div_by_zero, fr_div_works, fr_equal_works, fr_from_uint64_works, fr_is_null_works, + fr_is_one_works, fr_is_zero_works, fr_negate_works, fr_pow_works, fr_uint64s_roundtrip, + g1_identity_is_identity, g1_identity_is_infinity, g1_make_linear_combination, + g1_random_linear_combination, log_2_byte_works, p1_mul_works, p1_sub_works, + p2_add_or_dbl_works, p2_mul_works, p2_sub_works, pairings_work, + }; + + use rust_kzg_blst::kzg_proofs::{g1_linear_combination, pairings_verify}; + use rust_kzg_blst::types::fr::FsFr; + use rust_kzg_blst::types::g1::FsG1; + use rust_kzg_blst::types::g2::FsG2; #[test] - pub fn log_2_byte_works_() { - log_2_byte_works(&log_2_byte); + fn log_2_byte_works_() { + log_2_byte_works(&log_2_byte) } #[test] - pub fn fr_is_zero_works_() { - fr_is_zero_works::(); + fn fr_is_null_works_() { + fr_is_null_works::() } #[test] - pub fn fr_is_one_works_() { - fr_is_one_works::(); + fn fr_is_zero_works_() { + fr_is_zero_works::() } #[test] - pub fn fr_from_uint64_works_() { - fr_from_uint64_works::(); + fn fr_is_one_works_() { + fr_is_one_works::() } #[test] - pub fn fr_equal_works_() { - fr_equal_works::(); + fn fr_from_uint64_works_() { + fr_from_uint64_works::() } #[test] - pub fn fr_negate_works_() { - fr_negate_works::(); + fn fr_equal_works_() { + fr_equal_works::() } #[test] - pub fn fr_pow_works_() { - fr_pow_works::(); + fn fr_negate_works_() { + fr_negate_works::() } #[test] - pub fn fr_div_works_() { - fr_div_works::(); + fn fr_pow_works_() { + fr_pow_works::() } #[test] - #[should_panic] - pub fn fr_div_by_zero_() { - fr_div_by_zero::(); + fn fr_div_works_() { + fr_div_works::() } #[test] - pub fn fr_uint64s_roundtrip_() { - fr_uint64s_roundtrip::(); + fn fr_div_by_zero_() { + fr_div_by_zero::() } #[test] - pub fn p1_mul_works_() { - p1_mul_works::(); + fn fr_uint64s_roundtrip_() { + fr_uint64s_roundtrip::() } #[test] - pub fn p1_sub_works_() { - p1_sub_works::(); + fn p1_mul_works_() { + p1_mul_works::() } #[test] - pub fn p2_add_or_dbl_works_() { - p2_add_or_dbl_works::(); + fn p1_sub_works_() { + p1_sub_works::() } #[test] - pub fn p2_mul_works_() { - p2_mul_works::(); + fn p2_add_or_dbl_works_() { + p2_add_or_dbl_works::() } #[test] - pub fn p2_sub_works_() { - p2_sub_works::(); + fn p2_mul_works_() { + p2_mul_works::() } #[test] - pub fn g1_identity_is_infinity_() { - g1_identity_is_infinity::(); + fn p2_sub_works_() { + p2_sub_works::() } #[test] - pub fn g1_identity_is_identity_() { - g1_identity_is_identity::(); + fn g1_identity_is_infinity_() { + g1_identity_is_infinity::() } #[test] - pub fn g1_make_linear_combination_() { - g1_make_linear_combination::(&g1_linear_combination); + fn g1_identity_is_identity_() { + g1_identity_is_identity::() } #[test] - pub fn g1_random_linear_combination_() { - g1_random_linear_combination::(&g1_linear_combination); + fn g1_make_linear_combination_() { + g1_make_linear_combination::(&g1_linear_combination) } #[test] - pub fn pairings_work_() { - pairings_work::(&pairings_verify); + fn g1_random_linear_combination_() { + g1_random_linear_combination::(&g1_linear_combination) } #[test] - pub fn fr_is_null_works_() { - fr_is_null_works::(); + fn pairings_work_() { + pairings_work::(&pairings_verify) } } diff --git a/constantine/tests/c_bindings.rs b/constantine/tests/c_bindings.rs new file mode 100644 index 000000000..e08446a4e --- /dev/null +++ b/constantine/tests/c_bindings.rs @@ -0,0 +1,80 @@ +#[cfg(test)] +mod tests { + use kzg_bench::tests::c_bindings::{ + blob_to_kzg_commitment_invalid_blob_test, + compute_blob_kzg_proof_commitment_is_point_at_infinity_test, + compute_blob_kzg_proof_invalid_blob_test, free_trusted_setup_null_ptr_test, + free_trusted_setup_set_all_values_to_null_test, + load_trusted_setup_file_invalid_format_test, load_trusted_setup_file_valid_format_test, + load_trusted_setup_invalid_form_test, load_trusted_setup_invalid_g1_byte_length_test, + load_trusted_setup_invalid_g1_point_test, load_trusted_setup_invalid_g2_byte_length_test, + load_trusted_setup_invalid_g2_point_test, + }; + use rust_kzg_blst::eip_4844::{ + blob_to_kzg_commitment, compute_blob_kzg_proof, free_trusted_setup, load_trusted_setup, + load_trusted_setup_file, + }; + + #[test] + fn blob_to_kzg_commitment_invalid_blob() { + blob_to_kzg_commitment_invalid_blob_test(blob_to_kzg_commitment, load_trusted_setup_file); + } + + #[test] + fn load_trusted_setup_invalid_g1_byte_length() { + load_trusted_setup_invalid_g1_byte_length_test(load_trusted_setup); + } + + #[test] + fn load_trusted_setup_invalid_g1_point() { + load_trusted_setup_invalid_g1_point_test(load_trusted_setup); + } + + #[test] + fn load_trusted_setup_invalid_g2_byte_length() { + load_trusted_setup_invalid_g2_byte_length_test(load_trusted_setup); + } + + #[test] + fn load_trusted_setup_invalid_g2_point() { + load_trusted_setup_invalid_g2_point_test(load_trusted_setup); + } + + #[test] + fn load_trusted_setup_invalid_form() { + load_trusted_setup_invalid_form_test(load_trusted_setup); + } + + #[test] + fn load_trusted_setup_file_invalid_format() { + load_trusted_setup_file_invalid_format_test(load_trusted_setup_file); + } + + #[test] + fn load_trusted_setup_file_valid_format() { + load_trusted_setup_file_valid_format_test(load_trusted_setup_file); + } + + #[test] + fn free_trusted_setup_null_ptr() { + free_trusted_setup_null_ptr_test(free_trusted_setup); + } + + #[test] + fn free_trusted_setup_set_all_values_to_null() { + free_trusted_setup_set_all_values_to_null_test(free_trusted_setup, load_trusted_setup_file); + } + + #[test] + fn compute_blob_kzg_proof_invalid_blob() { + compute_blob_kzg_proof_invalid_blob_test(compute_blob_kzg_proof, load_trusted_setup_file); + } + + #[test] + fn compute_blob_kzg_proof_commitment_is_point_at_infinity() { + compute_blob_kzg_proof_commitment_is_point_at_infinity_test( + compute_blob_kzg_proof, + load_trusted_setup_file, + ); + } +} diff --git a/constantine/tests/consts.rs b/constantine/tests/consts.rs index 88966e4c2..e3b11b86d 100644 --- a/constantine/tests/consts.rs +++ b/constantine/tests/consts.rs @@ -1,36 +1,45 @@ +// #[path = "./local_tests/local_consts.rs"] +// pub mod local_consts; + #[cfg(test)] mod tests { use kzg_bench::tests::consts::{ expand_roots_is_plausible, new_fft_settings_is_plausible, roots_of_unity_are_plausible, roots_of_unity_is_the_expected_size, roots_of_unity_out_of_bounds_fails, }; - use rust_kzg_zkcrypto::consts::SCALE2_ROOT_OF_UNITY; - use rust_kzg_zkcrypto::kzg_proofs::expand_root_of_unity; - use rust_kzg_zkcrypto::kzg_proofs::FFTSettings; - use rust_kzg_zkcrypto::kzg_types::ZFr; + use rust_kzg_blst::consts::SCALE2_ROOT_OF_UNITY; + use rust_kzg_blst::types::fft_settings::{expand_root_of_unity, FsFFTSettings}; + use rust_kzg_blst::types::fr::FsFr; + + // Shared tests + #[test] + fn roots_of_unity_is_the_expected_size_() { + roots_of_unity_is_the_expected_size(&SCALE2_ROOT_OF_UNITY); + } #[test] fn roots_of_unity_out_of_bounds_fails_() { - roots_of_unity_out_of_bounds_fails::(); + roots_of_unity_out_of_bounds_fails::(); } #[test] fn roots_of_unity_are_plausible_() { - roots_of_unity_are_plausible::(&SCALE2_ROOT_OF_UNITY); + roots_of_unity_are_plausible::(&SCALE2_ROOT_OF_UNITY); } #[test] fn expand_roots_is_plausible_() { - expand_roots_is_plausible::(&SCALE2_ROOT_OF_UNITY, &expand_root_of_unity); + expand_roots_is_plausible::(&SCALE2_ROOT_OF_UNITY, &expand_root_of_unity); } #[test] fn new_fft_settings_is_plausible_() { - new_fft_settings_is_plausible::(); + new_fft_settings_is_plausible::(); } - #[test] - fn roots_of_unity_is_the_expected_size_() { - roots_of_unity_is_the_expected_size(&SCALE2_ROOT_OF_UNITY); - } + // Local tests + // #[test] + // fn roots_of_unity_repeat_at_stride_() { + // roots_of_unity_repeat_at_stride::(); + // } } diff --git a/constantine/tests/das.rs b/constantine/tests/das.rs index 0459a4f16..40e32ddf3 100644 --- a/constantine/tests/das.rs +++ b/constantine/tests/das.rs @@ -1,16 +1,16 @@ #[cfg(test)] mod tests { use kzg_bench::tests::das::{das_extension_test_known, das_extension_test_random}; - use rust_kzg_zkcrypto::kzg_proofs::FFTSettings; - use rust_kzg_zkcrypto::kzg_types::ZFr; + use rust_kzg_blst::types::fft_settings::FsFFTSettings; + use rust_kzg_blst::types::fr::FsFr; #[test] fn das_extension_test_known_() { - das_extension_test_known::(); + das_extension_test_known::(); } #[test] fn das_extension_test_random_() { - das_extension_test_random::(); + das_extension_test_random::(); } } diff --git a/constantine/tests/eip_4844.rs b/constantine/tests/eip_4844.rs index 333c9d9a7..cc2e5cd3c 100644 --- a/constantine/tests/eip_4844.rs +++ b/constantine/tests/eip_4844.rs @@ -7,40 +7,44 @@ mod tests { verify_blob_kzg_proof_rust, verify_kzg_proof_rust, }; use kzg::Fr; + use kzg_bench::tests::eip_4844::{ blob_to_kzg_commitment_test, bytes_to_bls_field_test, compute_and_verify_blob_kzg_proof_fails_with_incorrect_proof_test, compute_and_verify_blob_kzg_proof_test, compute_and_verify_kzg_proof_fails_with_incorrect_proof_test, - compute_and_verify_kzg_proof_round_trip_test, compute_kzg_proof_test, compute_powers_test, - verify_kzg_proof_batch_fails_with_incorrect_proof_test, verify_kzg_proof_batch_test, - }; - #[cfg(not(feature = "minimal-spec"))] - use kzg_bench::tests::eip_4844::{ - compute_and_verify_kzg_proof_within_domain_test, test_vectors_blob_to_kzg_commitment, + compute_and_verify_kzg_proof_round_trip_test, + compute_and_verify_kzg_proof_within_domain_test, compute_kzg_proof_empty_blob_vector_test, + compute_kzg_proof_incorrect_blob_length_test, + compute_kzg_proof_incorrect_commitments_len_test, + compute_kzg_proof_incorrect_poly_length_test, compute_kzg_proof_incorrect_proofs_len_test, + compute_kzg_proof_test, compute_powers_test, test_vectors_blob_to_kzg_commitment, test_vectors_compute_blob_kzg_proof, test_vectors_compute_kzg_proof, test_vectors_verify_blob_kzg_proof, test_vectors_verify_blob_kzg_proof_batch, - test_vectors_verify_kzg_proof, + test_vectors_verify_kzg_proof, validate_batched_input_test, + verify_kzg_proof_batch_fails_with_incorrect_proof_test, verify_kzg_proof_batch_test, + }; + use rust_kzg_blst::consts::SCALE2_ROOT_OF_UNITY; + use rust_kzg_blst::eip_4844::load_trusted_setup_filename_rust; + use rust_kzg_blst::types::fft_settings::expand_root_of_unity; + use rust_kzg_blst::types::{ + fft_settings::FsFFTSettings, fr::FsFr, g1::FsG1, g2::FsG2, kzg_settings::FsKZGSettings, + poly::FsPoly, }; - use rust_kzg_zkcrypto::consts::SCALE2_ROOT_OF_UNITY; - use rust_kzg_zkcrypto::eip_4844::load_trusted_setup_filename_rust; - use rust_kzg_zkcrypto::kzg_proofs::{expand_root_of_unity, FFTSettings, KZGSettings}; - use rust_kzg_zkcrypto::kzg_types::{ZFr, ZG1, ZG2}; - use rust_kzg_zkcrypto::poly::PolyData; #[test] pub fn bytes_to_bls_field_test_() { - bytes_to_bls_field_test::(); + bytes_to_bls_field_test::(); } #[test] pub fn compute_powers_test_() { - compute_powers_test::(&compute_powers); + compute_powers_test::(&compute_powers); } #[test] pub fn blob_to_kzg_commitment_test_() { - blob_to_kzg_commitment_test::( + blob_to_kzg_commitment_test::( &load_trusted_setup_filename_rust, &blob_to_kzg_commitment_rust, ); @@ -48,7 +52,7 @@ mod tests { #[test] pub fn compute_kzg_proof_test_() { - compute_kzg_proof_test::( + compute_kzg_proof_test::( &load_trusted_setup_filename_rust, &compute_kzg_proof_rust, &blob_to_polynomial, @@ -59,12 +63,12 @@ mod tests { #[test] pub fn compute_and_verify_kzg_proof_round_trip_test_() { compute_and_verify_kzg_proof_round_trip_test::< - ZFr, - ZG1, - ZG2, - PolyData, - FFTSettings, - KZGSettings, + FsFr, + FsG1, + FsG2, + FsPoly, + FsFFTSettings, + FsKZGSettings, >( &load_trusted_setup_filename_rust, &blob_to_kzg_commitment_rust, @@ -76,16 +80,15 @@ mod tests { ); } - #[cfg(not(feature = "minimal-spec"))] #[test] pub fn compute_and_verify_kzg_proof_within_domain_test_() { compute_and_verify_kzg_proof_within_domain_test::< - ZFr, - ZG1, - ZG2, - PolyData, - FFTSettings, - KZGSettings, + FsFr, + FsG1, + FsG2, + FsPoly, + FsFFTSettings, + FsKZGSettings, >( &load_trusted_setup_filename_rust, &blob_to_kzg_commitment_rust, @@ -100,12 +103,12 @@ mod tests { #[test] pub fn compute_and_verify_kzg_proof_fails_with_incorrect_proof_test_() { compute_and_verify_kzg_proof_fails_with_incorrect_proof_test::< - ZFr, - ZG1, - ZG2, - PolyData, - FFTSettings, - KZGSettings, + FsFr, + FsG1, + FsG2, + FsPoly, + FsFFTSettings, + FsKZGSettings, >( &load_trusted_setup_filename_rust, &blob_to_kzg_commitment_rust, @@ -119,7 +122,14 @@ mod tests { #[test] pub fn compute_and_verify_blob_kzg_proof_test_() { - compute_and_verify_blob_kzg_proof_test::( + compute_and_verify_blob_kzg_proof_test::< + FsFr, + FsG1, + FsG2, + FsPoly, + FsFFTSettings, + FsKZGSettings, + >( &load_trusted_setup_filename_rust, &blob_to_kzg_commitment_rust, &bytes_to_blob, @@ -131,12 +141,12 @@ mod tests { #[test] pub fn compute_and_verify_blob_kzg_proof_fails_with_incorrect_proof_test_() { compute_and_verify_blob_kzg_proof_fails_with_incorrect_proof_test::< - ZFr, - ZG1, - ZG2, - PolyData, - FFTSettings, - KZGSettings, + FsFr, + FsG1, + FsG2, + FsPoly, + FsFFTSettings, + FsKZGSettings, >( &load_trusted_setup_filename_rust, &blob_to_kzg_commitment_rust, @@ -148,7 +158,7 @@ mod tests { #[test] pub fn verify_kzg_proof_batch_test_() { - verify_kzg_proof_batch_test::( + verify_kzg_proof_batch_test::( &load_trusted_setup_filename_rust, &blob_to_kzg_commitment_rust, &bytes_to_blob, @@ -160,12 +170,12 @@ mod tests { #[test] pub fn verify_kzg_proof_batch_fails_with_incorrect_proof_test_() { verify_kzg_proof_batch_fails_with_incorrect_proof_test::< - ZFr, - ZG1, - ZG2, - PolyData, - FFTSettings, - KZGSettings, + FsFr, + FsG1, + FsG2, + FsPoly, + FsFFTSettings, + FsKZGSettings, >( &load_trusted_setup_filename_rust, &blob_to_kzg_commitment_rust, @@ -175,58 +185,60 @@ mod tests { ); } - #[cfg(not(feature = "minimal-spec"))] #[test] pub fn test_vectors_blob_to_kzg_commitment_() { - test_vectors_blob_to_kzg_commitment::( + test_vectors_blob_to_kzg_commitment::( &load_trusted_setup_filename_rust, &blob_to_kzg_commitment_rust, &bytes_to_blob, ); } - #[cfg(not(feature = "minimal-spec"))] #[test] pub fn test_vectors_compute_kzg_proof_() { - test_vectors_compute_kzg_proof::( + test_vectors_compute_kzg_proof::( &load_trusted_setup_filename_rust, &compute_kzg_proof_rust, &bytes_to_blob, ); } - #[cfg(not(feature = "minimal-spec"))] #[test] pub fn test_vectors_compute_blob_kzg_proof_() { - test_vectors_compute_blob_kzg_proof::( + test_vectors_compute_blob_kzg_proof::( &load_trusted_setup_filename_rust, &bytes_to_blob, &compute_blob_kzg_proof_rust, ); } - #[cfg(not(feature = "minimal-spec"))] #[test] pub fn test_vectors_verify_kzg_proof_() { - test_vectors_verify_kzg_proof::( + test_vectors_verify_kzg_proof::( &load_trusted_setup_filename_rust, &verify_kzg_proof_rust, ); } - #[cfg(not(feature = "minimal-spec"))] #[test] pub fn test_vectors_verify_blob_kzg_proof_() { - test_vectors_verify_blob_kzg_proof::( + test_vectors_verify_blob_kzg_proof::( &load_trusted_setup_filename_rust, &bytes_to_blob, &verify_blob_kzg_proof_rust, ); } - #[cfg(not(feature = "minimal-spec"))] + #[test] pub fn test_vectors_verify_blob_kzg_proof_batch_() { - test_vectors_verify_blob_kzg_proof_batch::( + test_vectors_verify_blob_kzg_proof_batch::< + FsFr, + FsG1, + FsG2, + FsPoly, + FsFFTSettings, + FsKZGSettings, + >( &load_trusted_setup_filename_rust, &bytes_to_blob, &verify_blob_kzg_proof_batch_rust, @@ -235,13 +247,74 @@ mod tests { #[test] pub fn expand_root_of_unity_too_long() { - let out = expand_root_of_unity(&ZFr::from_u64_arr(&SCALE2_ROOT_OF_UNITY[1]), 1); + let out = expand_root_of_unity(&FsFr::from_u64_arr(&SCALE2_ROOT_OF_UNITY[1]), 1); assert!(out.is_err()); } #[test] pub fn expand_root_of_unity_too_short() { - let out = expand_root_of_unity(&ZFr::from_u64_arr(&SCALE2_ROOT_OF_UNITY[1]), 3); + let out = expand_root_of_unity(&FsFr::from_u64_arr(&SCALE2_ROOT_OF_UNITY[1]), 3); assert!(out.is_err()); } + + #[test] + pub fn compute_kzg_proof_incorrect_blob_length() { + compute_kzg_proof_incorrect_blob_length_test::(&blob_to_polynomial); + } + + #[test] + pub fn compute_kzg_proof_incorrect_poly_length() { + compute_kzg_proof_incorrect_poly_length_test::< + FsPoly, + FsFr, + FsG1, + FsG2, + FsFFTSettings, + FsKZGSettings, + >(&evaluate_polynomial_in_evaluation_form); + } + + #[test] + pub fn compute_kzg_proof_empty_blob_vector() { + compute_kzg_proof_empty_blob_vector_test::< + FsPoly, + FsFr, + FsG1, + FsG2, + FsFFTSettings, + FsKZGSettings, + >(&verify_blob_kzg_proof_batch_rust) + } + + #[test] + pub fn compute_kzg_proof_incorrect_commitments_len() { + compute_kzg_proof_incorrect_commitments_len_test::< + FsPoly, + FsFr, + FsG1, + FsG2, + FsFFTSettings, + FsKZGSettings, + >(&verify_blob_kzg_proof_batch_rust) + } + + #[test] + pub fn compute_kzg_proof_incorrect_proofs_len() { + compute_kzg_proof_incorrect_proofs_len_test::< + FsPoly, + FsFr, + FsG1, + FsG2, + FsFFTSettings, + FsKZGSettings, + >(&verify_blob_kzg_proof_batch_rust) + } + + #[test] + pub fn validate_batched_input() { + validate_batched_input_test::( + &verify_blob_kzg_proof_batch_rust, + &load_trusted_setup_filename_rust, + ) + } } diff --git a/constantine/tests/fft_fr.rs b/constantine/tests/fft_fr.rs index 78228eefa..cfe48acb8 100644 --- a/constantine/tests/fft_fr.rs +++ b/constantine/tests/fft_fr.rs @@ -1,27 +1,27 @@ #[cfg(test)] mod tests { use kzg_bench::tests::fft_fr::{compare_sft_fft, inverse_fft, roundtrip_fft, stride_fft}; - use rust_kzg_zkcrypto::fft::{fft_fr_fast, fft_fr_slow}; - use rust_kzg_zkcrypto::kzg_proofs::FFTSettings; - use rust_kzg_zkcrypto::kzg_types::ZFr; + use rust_kzg_blst::fft_fr::{fft_fr_fast, fft_fr_slow}; + use rust_kzg_blst::types::fft_settings::FsFFTSettings; + use rust_kzg_blst::types::fr::FsFr; #[test] fn compare_sft_fft_() { - compare_sft_fft::(&fft_fr_slow, &fft_fr_fast); + compare_sft_fft::(&fft_fr_slow, &fft_fr_fast); } #[test] fn roundtrip_fft_() { - roundtrip_fft::(); + roundtrip_fft::(); } #[test] fn inverse_fft_() { - inverse_fft::(); + inverse_fft::(); } #[test] fn stride_fft_() { - stride_fft::(); + stride_fft::(); } } diff --git a/constantine/tests/fft_g1.rs b/constantine/tests/fft_g1.rs index 6d2035557..cf0f079aa 100644 --- a/constantine/tests/fft_g1.rs +++ b/constantine/tests/fft_g1.rs @@ -1,22 +1,38 @@ #[cfg(test)] mod tests { - use kzg_bench::tests::fft_g1::{compare_sft_fft, roundtrip_fft, stride_fft}; - use rust_kzg_zkcrypto::fft_g1::{fft_g1_fast, fft_g1_slow, make_data}; - use rust_kzg_zkcrypto::kzg_proofs::FFTSettings; - use rust_kzg_zkcrypto::kzg_types::{ZFr, ZG1}; + use kzg::G1; + use kzg_bench::tests::fft_g1::{compare_ft_fft, roundtrip_fft, stride_fft}; + use rust_kzg_blst::consts::G1_GENERATOR; + use rust_kzg_blst::fft_g1::{fft_g1_fast, fft_g1_slow}; + use rust_kzg_blst::types::fft_settings::FsFFTSettings; + use rust_kzg_blst::types::fr::FsFr; + use rust_kzg_blst::types::g1::FsG1; + + fn make_data(n: usize) -> Vec { + if n == 0 { + return Vec::new(); + } + let mut result: Vec = vec![FsG1::default(); n]; + result[0] = G1_GENERATOR; + for i in 1..n { + result[i] = result[i - 1].add_or_dbl(&G1_GENERATOR) + } + + result + } #[test] fn roundtrip_fft_() { - roundtrip_fft::(&make_data); + roundtrip_fft::(&make_data); } #[test] fn stride_fft_() { - stride_fft::(&make_data); + stride_fft::(&make_data); } #[test] fn compare_sft_fft_() { - compare_sft_fft::(&fft_g1_slow, &fft_g1_fast, &make_data); + compare_ft_fft::(&fft_g1_slow, &fft_g1_fast, &make_data); } } diff --git a/constantine/tests/fk20_proofs.rs b/constantine/tests/fk20_proofs.rs index 1c9e1993a..bb241aaf3 100644 --- a/constantine/tests/fk20_proofs.rs +++ b/constantine/tests/fk20_proofs.rs @@ -1,16 +1,19 @@ #[cfg(test)] mod tests { use kzg_bench::tests::fk20_proofs::*; - - use rust_kzg_zkcrypto::fk20_proofs::{KzgFK20MultiSettings, KzgFK20SingleSettings}; - use rust_kzg_zkcrypto::kzg_proofs::{generate_trusted_setup, FFTSettings, KZGSettings}; - use rust_kzg_zkcrypto::kzg_types::ZFr as BlstFr; - use rust_kzg_zkcrypto::kzg_types::{ZG1, ZG2}; - use rust_kzg_zkcrypto::poly::PolyData; + use rust_kzg_blst::types::fft_settings::FsFFTSettings; + use rust_kzg_blst::types::fk20_multi_settings::FsFK20MultiSettings; + use rust_kzg_blst::types::fk20_single_settings::FsFK20SingleSettings; + use rust_kzg_blst::types::fr::FsFr; + use rust_kzg_blst::types::g1::FsG1; + use rust_kzg_blst::types::g2::FsG2; + use rust_kzg_blst::types::kzg_settings::FsKZGSettings; + use rust_kzg_blst::types::poly::FsPoly; + use rust_kzg_blst::utils::generate_trusted_setup; #[test] fn test_fk_single() { - fk_single::( + fk_single::( &generate_trusted_setup, ); } @@ -18,65 +21,65 @@ mod tests { #[test] fn test_fk_single_strided() { fk_single_strided::< - BlstFr, - ZG1, - ZG2, - PolyData, - FFTSettings, - KZGSettings, - KzgFK20SingleSettings, + FsFr, + FsG1, + FsG2, + FsPoly, + FsFFTSettings, + FsKZGSettings, + FsFK20SingleSettings, >(&generate_trusted_setup); } #[test] fn test_fk_multi_settings() { fk_multi_settings::< - BlstFr, - ZG1, - ZG2, - PolyData, - FFTSettings, - KZGSettings, - KzgFK20MultiSettings, + FsFr, + FsG1, + FsG2, + FsPoly, + FsFFTSettings, + FsKZGSettings, + FsFK20MultiSettings, >(&generate_trusted_setup); } #[test] fn test_fk_multi_chunk_len_1_512() { fk_multi_chunk_len_1_512::< - BlstFr, - ZG1, - ZG2, - PolyData, - FFTSettings, - KZGSettings, - KzgFK20MultiSettings, + FsFr, + FsG1, + FsG2, + FsPoly, + FsFFTSettings, + FsKZGSettings, + FsFK20MultiSettings, >(&generate_trusted_setup); } #[test] fn test_fk_multi_chunk_len_16_512() { fk_multi_chunk_len_16_512::< - BlstFr, - ZG1, - ZG2, - PolyData, - FFTSettings, - KZGSettings, - KzgFK20MultiSettings, + FsFr, + FsG1, + FsG2, + FsPoly, + FsFFTSettings, + FsKZGSettings, + FsFK20MultiSettings, >(&generate_trusted_setup); } #[test] fn test_fk_multi_chunk_len_16_16() { fk_multi_chunk_len_16_16::< - BlstFr, - ZG1, - ZG2, - PolyData, - FFTSettings, - KZGSettings, - KzgFK20MultiSettings, + FsFr, + FsG1, + FsG2, + FsPoly, + FsFFTSettings, + FsKZGSettings, + FsFK20MultiSettings, >(&generate_trusted_setup); } } diff --git a/constantine/tests/kzg_proofs.rs b/constantine/tests/kzg_proofs.rs index 6c3abf607..9e0676d1b 100644 --- a/constantine/tests/kzg_proofs.rs +++ b/constantine/tests/kzg_proofs.rs @@ -1,31 +1,100 @@ #[cfg(test)] mod tests { + use blst::{ + blst_final_exp, blst_fp12, blst_fp12_mul, blst_miller_loop, blst_p1_affine, blst_p1_cneg, + blst_p1_to_affine, blst_p2_affine, blst_p2_to_affine, Pairing, + }; + use kzg::G1; use kzg_bench::tests::kzg_proofs::{ commit_to_nil_poly, commit_to_too_long_poly_returns_err, proof_multi, proof_single, }; - use rust_kzg_zkcrypto::kzg_proofs::{generate_trusted_setup, FFTSettings, KZGSettings}; - use rust_kzg_zkcrypto::kzg_types::{ZFr, ZG1, ZG2}; - use rust_kzg_zkcrypto::poly::PolyData; + + use rust_kzg_blst::types::fft_settings::FsFFTSettings; + use rust_kzg_blst::types::fr::FsFr; + use rust_kzg_blst::types::g1::FsG1; + use rust_kzg_blst::types::g2::FsG2; + use rust_kzg_blst::types::kzg_settings::FsKZGSettings; + use rust_kzg_blst::types::poly::FsPoly; + use rust_kzg_blst::utils::generate_trusted_setup; #[test] - fn proof_single_() { - proof_single::(&generate_trusted_setup); + pub fn test_proof_single() { + proof_single::( + &generate_trusted_setup, + ); } + #[test] - fn commit_to_nil_poly_() { - commit_to_nil_poly::( + pub fn test_commit_to_nil_poly() { + commit_to_nil_poly::( &generate_trusted_setup, ); } + + #[test] + pub fn test_commit_to_too_long_poly() { + commit_to_too_long_poly_returns_err::( + &generate_trusted_setup, + ); + } + #[test] - fn commit_to_too_long_poly_() { - commit_to_too_long_poly_returns_err::( + pub fn test_proof_multi() { + proof_multi::( &generate_trusted_setup, ); } + // This aims at showing that the use of the blst::Pairing engine in pairings_verify + // has the desired semantics. + #[cfg(feature = "rand")] + fn og_pairings_verify() { + let a1 = FsG1::rand(); + let a2 = FsG2::rand(); + let b1 = FsG1::rand(); + let b2 = FsG2::rand(); + + let mut loop0 = blst_fp12::default(); + let mut loop1 = blst_fp12::default(); + let mut gt_point = blst_fp12::default(); + + let mut aa1 = blst_p1_affine::default(); + let mut bb1 = blst_p1_affine::default(); + + let mut aa2 = blst_p2_affine::default(); + let mut bb2 = blst_p2_affine::default(); + + // As an optimisation, we want to invert one of the pairings, + // so we negate one of the points. + let mut a1neg: FsG1 = a1; + unsafe { + blst_p1_cneg(&mut a1neg.0, true); + blst_p1_to_affine(&mut aa1, &a1neg.0); + + blst_p1_to_affine(&mut bb1, &b1.0); + blst_p2_to_affine(&mut aa2, &a2.0); + blst_p2_to_affine(&mut bb2, &b2.0); + + blst_miller_loop(&mut loop0, &aa2, &aa1); + blst_miller_loop(&mut loop1, &bb2, &bb1); + + blst_fp12_mul(&mut gt_point, &loop0, &loop1); + blst_final_exp(&mut gt_point, >_point); + + let dst = [0u8; 3]; + let mut pairing_blst = Pairing::new(false, &dst); + pairing_blst.raw_aggregate(&aa2, &aa1); + pairing_blst.raw_aggregate(&bb2, &bb1); + + assert_eq!(gt_point, pairing_blst.as_fp12().final_exp()); + } + } + + #[cfg(feature = "rand")] #[test] - fn proof_multi_() { - proof_multi::(&generate_trusted_setup); + pub fn test_pairings_verify() { + for _i in 0..100 { + og_pairings_verify(); + } } } diff --git a/constantine/tests/local_tests/local_consts.rs b/constantine/tests/local_tests/local_consts.rs new file mode 100644 index 000000000..450d8c765 --- /dev/null +++ b/constantine/tests/local_tests/local_consts.rs @@ -0,0 +1,16 @@ +use kzg::{FFTSettings, Fr}; + +pub fn roots_of_unity_repeat_at_stride>() { + let fs1 = TFFTSettings::new(15).unwrap(); + let fs2 = TFFTSettings::new(16).unwrap(); + let fs3 = TFFTSettings::new(17).unwrap(); + + for i in 0..fs1.get_max_width() { + assert!(fs1 + .get_expanded_roots_of_unity_at(i) + .equals(&fs2.get_expanded_roots_of_unity_at(i * 2))); + assert!(fs1 + .get_expanded_roots_of_unity_at(i) + .equals(&fs3.get_expanded_roots_of_unity_at(i * 4))); + } +} diff --git a/constantine/tests/local_tests/local_poly.rs b/constantine/tests/local_tests/local_poly.rs new file mode 100644 index 000000000..4c4f290ae --- /dev/null +++ b/constantine/tests/local_tests/local_poly.rs @@ -0,0 +1,371 @@ +use kzg::{Fr, Poly}; +use rand::rngs::StdRng; +use rand::{RngCore, SeedableRng}; + +use rust_kzg_blst::types::fr::FsFr; +use rust_kzg_blst::types::poly::FsPoly; + +pub fn create_poly_of_length_ten() { + let poly = FsPoly::new(10); + assert_eq!(poly.len(), 10); +} + +pub fn poly_pad_works_rand() { + let mut rng = StdRng::seed_from_u64(0); + + for _k in 0..256 { + let poly_length: usize = (1 + (rng.next_u64() % 1000)) as usize; + let mut poly = FsPoly::new(poly_length); + for i in 0..poly.len() { + poly.set_coeff_at(i, &FsFr::rand()); + } + + let padded_poly = poly.pad(1000); + for i in 0..poly_length { + assert!(padded_poly.get_coeff_at(i).equals(&poly.get_coeff_at(i))); + } + for i in poly_length..1000 { + assert!(padded_poly.get_coeff_at(i).equals(&Fr::zero())); + } + } +} + +pub fn poly_eval_check() { + let n: usize = 10; + let mut poly = FsPoly::new(n); + for i in 0..n { + let fr = FsFr::from_u64((i + 1) as u64); + poly.set_coeff_at(i, &fr); + } + let expected = FsFr::from_u64((n * (n + 1) / 2) as u64); + let actual = poly.eval(&FsFr::one()); + assert!(expected.equals(&actual)); +} + +pub fn poly_eval_0_check() { + let n: usize = 7; + let a: usize = 597; + let mut poly = FsPoly::new(n); + for i in 0..n { + let fr = FsFr::from_u64((i + a) as u64); + poly.set_coeff_at(i, &fr); + } + let expected = FsFr::from_u64(a as u64); + let actual = poly.eval(&FsFr::zero()); + assert!(expected.equals(&actual)); +} + +pub fn poly_eval_nil_check() { + let n: usize = 0; + let poly = FsPoly::new(n); + let actual = poly.eval(&FsFr::one()); + assert!(actual.equals(&FsFr::zero())); +} + +pub fn poly_inverse_simple_0() { + // 1 / (1 - x) = 1 + x + x^2 + ... + let d: usize = 16; + let mut p = FsPoly::new(2); + p.set_coeff_at(0, &FsFr::one()); + p.set_coeff_at(1, &FsFr::one()); + p.set_coeff_at(1, &FsFr::negate(&p.get_coeff_at(1))); + let result = p.inverse(d); + assert!(result.is_ok()); + let q = result.unwrap(); + for i in 0..d { + assert!(q.get_coeff_at(i).is_one()); + } +} + +pub fn poly_inverse_simple_1() { + // 1 / (1 + x) = 1 - x + x^2 - ... + let d: usize = 16; + let mut p = FsPoly::new(2); + p.set_coeff_at(0, &FsFr::one()); + p.set_coeff_at(1, &FsFr::one()); + let result = p.inverse(d); + assert!(result.is_ok()); + let q = result.unwrap(); + for i in 0..d { + let mut tmp = q.get_coeff_at(i); + if i & 1 != 0 { + tmp = FsFr::negate(&tmp); + } + assert!(tmp.is_one()); + } +} + +pub const NUM_TESTS: u64 = 10; + +fn test_data(a: usize, b: usize) -> Vec { + // (x^2 - 1) / (x + 1) = x - 1 + let test_0_0 = vec![-1, 0, 1]; + let test_0_1 = vec![1, 1]; + let test_0_2 = vec![-1, 1]; + + // (12x^3 - 11x^2 + 9x + 18) / (4x + 3) = 3x^2 - 5x + 6 + let test_1_0 = vec![18, 9, -11, 12]; + let test_1_1 = vec![3, 4]; + let test_1_2 = vec![6, -5, 3]; + + // (x + 1) / (x^2 - 1) = nil + let test_2_0 = vec![1, 1]; + let test_2_1 = vec![-1, 0, 2]; + let test_2_2 = vec![]; + + // (10x^2 + 20x + 30) / 10 = x^2 + 2x + 3 + let test_3_0 = vec![30, 20, 10]; + let test_3_1 = vec![10]; + let test_3_2 = vec![3, 2, 1]; + + // (x^2 + x) / (x + 1) = x + let test_4_0 = vec![0, 1, 1]; + let test_4_1 = vec![1, 1]; + let test_4_2 = vec![0, 1]; + + // (x^2 + x + 1) / 1 = x^2 + x + 1 + let test_5_0 = vec![1, 1, 1]; + let test_5_1 = vec![1]; + let test_5_2 = vec![1, 1, 1]; + + // (x^2 + x + 1) / (0x + 1) = x^2 + x + 1 + let test_6_0 = vec![1, 1, 1]; + let test_6_1 = vec![1, 0]; // The highest coefficient is zero + let test_6_2 = vec![1, 1, 1]; + + // (x^3) / (x) = (x^2) + let test_7_0 = vec![0, 0, 0, 1]; + let test_7_1 = vec![0, 1]; + let test_7_2 = vec![0, 0, 1]; + + // + let test_8_0 = vec![ + 236, + 945, + -297698, + 2489425, + -18556462, + -301325440, + 2473062655, + -20699887353, + ]; + let test_8_1 = vec![4, 11, -5000, 45541, -454533]; + let test_8_2 = vec![59, 74, -878, 45541]; + + // (x^4 + 2x^3 + 3x^2 + 2x + 1) / (-x^2 -x -1) = (-x^2 -x -1) + let test_9_0 = vec![1, 2, 3, 2, 1]; + let test_9_1 = vec![-1, -1, -1]; + let test_9_2 = vec![-1, -1, -1]; + + let test_data = [ + [test_0_0, test_0_1, test_0_2], + [test_1_0, test_1_1, test_1_2], + [test_2_0, test_2_1, test_2_2], + [test_3_0, test_3_1, test_3_2], + [test_4_0, test_4_1, test_4_2], + [test_5_0, test_5_1, test_5_2], + [test_6_0, test_6_1, test_6_2], + [test_7_0, test_7_1, test_7_2], + [test_8_0, test_8_1, test_8_2], + [test_9_0, test_9_1, test_9_2], + ]; + + test_data[a][b].clone() +} + +fn new_test_poly(coeffs: &[i64]) -> FsPoly { + let mut p = FsPoly::new(0); + + for &coeff in coeffs.iter() { + if coeff >= 0 { + let c = FsFr::from_u64(coeff as u64); + p.coeffs.push(c); + } else { + let c = FsFr::from_u64((-coeff) as u64); + let negc = c.negate(); + p.coeffs.push(negc); + } + } + + p +} + +pub fn poly_div_long_test() { + for i in 0..9 { + // Tests are designed to throw an exception when last member is 0 + if i == 6 { + continue; + } + + let divided_data = test_data(i, 0); + let divisor_data = test_data(i, 1); + let expected_data = test_data(i, 2); + let mut dividend: FsPoly = new_test_poly(÷d_data); + let divisor: FsPoly = new_test_poly(&divisor_data); + let expected: FsPoly = new_test_poly(&expected_data); + + let actual = dividend.long_div(&divisor).unwrap(); + + assert_eq!(expected.len(), actual.len()); + for i in 0..actual.len() { + assert!(expected.get_coeff_at(i).equals(&actual.get_coeff_at(i))) + } + } +} + +pub fn poly_div_fast_test() { + for i in 0..9 { + // Tests are designed to throw an exception when last member is 0 + if i == 6 { + continue; + } + + let divided_data = test_data(i, 0); + let divisor_data = test_data(i, 1); + let expected_data = test_data(i, 2); + let mut dividend: FsPoly = new_test_poly(÷d_data); + let divisor: FsPoly = new_test_poly(&divisor_data); + let expected: FsPoly = new_test_poly(&expected_data); + + let actual = dividend.fast_div(&divisor).unwrap(); + + assert_eq!(expected.len(), actual.len()); + for i in 0..actual.len() { + assert!(expected.get_coeff_at(i).equals(&actual.get_coeff_at(i))) + } + } +} + +pub fn test_poly_div_by_zero() { + let mut dividend = FsPoly::new(2); + + dividend.set_coeff_at(0, &FsFr::from_u64(1)); + dividend.set_coeff_at(1, &FsFr::from_u64(1)); + + let divisor = FsPoly::new(0); + + let dummy = dividend.div(&divisor); + assert!(dummy.is_err()); +} + +pub fn poly_mul_direct_test() { + for i in 0..9 { + let coeffs1 = test_data(i, 2); + let coeffs2 = test_data(i, 1); + let coeffs3 = test_data(i, 0); + + let mut multiplicand: FsPoly = new_test_poly(&coeffs1); + let mut multiplier: FsPoly = new_test_poly(&coeffs2); + let expected: FsPoly = new_test_poly(&coeffs3); + + let result0 = multiplicand.mul_direct(&multiplier, coeffs3.len()).unwrap(); + for j in 0..result0.len() { + assert!(expected.get_coeff_at(j).equals(&result0.get_coeff_at(j))) + } + + // Check commutativity + let result1 = multiplier.mul_direct(&multiplicand, coeffs3.len()).unwrap(); + for j in 0..result1.len() { + assert!(expected.get_coeff_at(j).equals(&result1.get_coeff_at(j))) + } + } +} + +pub fn poly_mul_fft_test() { + for i in 0..9 { + // Ignore 0 multiplication case because its incorrect when multiplied backwards + if i == 2 { + continue; + } + + let coeffs1 = test_data(i, 2); + let coeffs2 = test_data(i, 1); + let coeffs3 = test_data(i, 0); + + let multiplicand: FsPoly = new_test_poly(&coeffs1); + let multiplier: FsPoly = new_test_poly(&coeffs2); + let expected: FsPoly = new_test_poly(&coeffs3); + + let result0 = multiplicand.mul_fft(&multiplier, coeffs3.len()).unwrap(); + for j in 0..result0.len() { + assert!(expected.get_coeff_at(j).equals(&result0.get_coeff_at(j))) + } + + // Check commutativity + let result1 = multiplier.mul_fft(&multiplicand, coeffs3.len()).unwrap(); + for j in 0..result1.len() { + assert!(expected.get_coeff_at(j).equals(&result1.get_coeff_at(j))) + } + } +} + +pub fn poly_mul_random() { + let mut rng = StdRng::seed_from_u64(0); + + for _k in 0..256 { + let multiplicand_length: usize = (1 + (rng.next_u64() % 1000)) as usize; + let mut multiplicand = FsPoly::new(multiplicand_length); + for i in 0..multiplicand.len() { + multiplicand.set_coeff_at(i, &FsFr::rand()); + } + + let multiplier_length: usize = (1 + (rng.next_u64() % 1000)) as usize; + let mut multiplier = FsPoly::new(multiplier_length); + for i in 0..multiplier.len() { + multiplier.set_coeff_at(i, &FsFr::rand()); + } + + if multiplicand.get_coeff_at(multiplicand.len() - 1).is_zero() { + multiplicand.set_coeff_at(multiplicand.len() - 1, &Fr::one()); + } + + if multiplier.get_coeff_at(multiplier.len() - 1).is_zero() { + multiplier.set_coeff_at(multiplier.len() - 1, &Fr::one()); + } + + let out_length: usize = (1 + (rng.next_u64() % 1000)) as usize; + let q0 = multiplicand.mul_direct(&multiplier, out_length).unwrap(); + let q1 = multiplicand.mul_fft(&multiplier, out_length).unwrap(); + + assert_eq!(q0.len(), q1.len()); + for i in 0..q0.len() { + assert!(q0.get_coeff_at(i).equals(&q1.get_coeff_at(i))); + } + } +} + +pub fn poly_div_random() { + let mut rng = StdRng::seed_from_u64(0); + for _k in 0..256 { + let dividend_length: usize = (2 + (rng.next_u64() % 1000)) as usize; + let divisor_length: usize = 1 + ((rng.next_u64() as usize) % dividend_length); + + let mut dividend = FsPoly::new(dividend_length); + let mut divisor = FsPoly::new(divisor_length); + + for i in 0..dividend_length { + dividend.set_coeff_at(i, &FsFr::rand()); + } + + for i in 0..divisor_length { + divisor.set_coeff_at(i, &FsFr::rand()); + } + + //Ensure that the polynomials' orders corresponds to their lengths + if dividend.get_coeff_at(dividend.len() - 1).is_zero() { + dividend.set_coeff_at(dividend.len() - 1, &Fr::one()); + } + + if divisor.get_coeff_at(divisor.len() - 1).is_zero() { + divisor.set_coeff_at(divisor.len() - 1, &Fr::one()); + } + + let result0 = dividend.long_div(&divisor).unwrap(); + let result1 = dividend.fast_div(&divisor).unwrap(); + + assert_eq!(result0.len(), result1.len()); + for i in 0..result0.len() { + assert!(result0.get_coeff_at(i).equals(&result1.get_coeff_at(i))); + } + } +} diff --git a/constantine/tests/local_tests/local_recovery.rs b/constantine/tests/local_tests/local_recovery.rs new file mode 100644 index 000000000..3de2e1f46 --- /dev/null +++ b/constantine/tests/local_tests/local_recovery.rs @@ -0,0 +1,158 @@ +use kzg::{FFTFr, FFTSettings, Fr, Poly, PolyRecover}; +use rand::rngs::StdRng; +use rand::{RngCore, SeedableRng}; +use std::convert::TryInto; + +fn shuffle(a: &mut [usize], n: usize) { + let mut i: u64 = n as u64; + let mut j: usize; + let mut tmp: usize; + + let mut rng = StdRng::seed_from_u64(0); + while i > 0 { + j = (rng.next_u64() % i) as usize; + i -= 1; + tmp = a[j]; + a[j] = a[i as usize]; + a[i as usize] = tmp; + } +} + +fn random_missing( + with_missing: &mut [Option], + data: &[TFr], + len_data: usize, + known: usize, +) { + let mut missing_idx = Vec::new(); + for i in 0..len_data { + missing_idx.push(i); + with_missing[i] = Some(data[i].clone()); + } + + shuffle(&mut missing_idx, len_data); + for i in 0..(len_data - known) { + with_missing[missing_idx[i]] = None; + } +} + +pub fn recover_simple< + TFr: Fr, + TFFTSettings: FFTSettings + FFTFr, + TPoly: Poly, + TPolyRecover: PolyRecover, +>() { + let fs_query = TFFTSettings::new(2); + assert!(fs_query.is_ok()); + + let fs: TFFTSettings = fs_query.unwrap(); + let max_width: usize = fs.get_max_width(); + + let poly_query = TPoly::new(max_width); + let mut poly = poly_query; + + for i in 0..(max_width / 2) { + poly.set_coeff_at(i, &TFr::from_u64(i.try_into().unwrap())); + } + + for i in (max_width / 2)..max_width { + poly.set_coeff_at(i, &TFr::zero()); + } + + let data_query = fs.fft_fr(poly.get_coeffs(), false); + assert!(data_query.is_ok()); + let data = data_query.unwrap(); + + let sample: [Option; 4] = [Some(data[0].clone()), None, None, Some(data[3].clone())]; + + let recovered_query = TPolyRecover::recover_poly_from_samples(&sample, &fs); + assert!(recovered_query.is_ok()); + let recovered = recovered_query.unwrap(); + + for (i, item) in data.iter().enumerate().take(max_width) { + assert!(item.equals(&recovered.get_coeff_at(i))); + } + + let mut recovered_vec: Vec = Vec::new(); + + for i in 0..max_width { + recovered_vec.push(recovered.get_coeff_at(i)); + } + + let back_query = fs.fft_fr(&recovered_vec, true); + assert!(back_query.is_ok()); + let back = back_query.unwrap(); + + for (i, back_x) in back[..(max_width / 2)].iter().enumerate() { + assert!(back_x.equals(&poly.get_coeff_at(i))); + } + + for back_x in back[(max_width / 2)..max_width].iter() { + assert!(back_x.is_zero()); + } +} + +pub fn recover_random< + TFr: Fr, + TFFTSettings: FFTSettings + FFTFr, + TPoly: Poly, + TPolyRecover: PolyRecover, +>() { + let fs_query = TFFTSettings::new(12); + assert!(fs_query.is_ok()); + + let fs: TFFTSettings = fs_query.unwrap(); + let max_width: usize = fs.get_max_width(); + // let mut poly = TPoly::default(); + let poly_query = TPoly::new(max_width); + let mut poly = poly_query; + + for i in 0..(max_width / 2) { + poly.set_coeff_at(i, &TFr::from_u64(i.try_into().unwrap())); + } + + for i in (max_width / 2)..max_width { + poly.set_coeff_at(i, &TFr::zero()); + } + + let data_query = fs.fft_fr(poly.get_coeffs(), false); + assert!(data_query.is_ok()); + let data = data_query.unwrap(); + + let mut samples = vec![Some(TFr::default()); max_width]; // std::vec![TFr; max_width]; + + for i in 0..10 { + let known_ratio = 0.5 + (i as f32) * 0.05; + let known: usize = ((max_width as f32) * known_ratio) as usize; + + for _ in 0..4 { + random_missing(&mut samples, &data, max_width, known); + + let recovered_query = TPolyRecover::recover_poly_from_samples(&samples, &fs); + assert!(recovered_query.is_ok()); + let recovered = recovered_query.unwrap(); + + for (j, item) in data.iter().enumerate().take(max_width) { + assert!(item.equals(&recovered.get_coeff_at(j))); + } + + let mut recovered_vec: Vec = Vec::new(); + + for i in 0..max_width { + recovered_vec.push(recovered.get_coeff_at(i)); + } + + let back_query = fs.fft_fr(&recovered_vec, true); + assert!(back_query.is_ok()); + let back = back_query.unwrap(); + + for (i, back_x) in back[..(max_width / 2)].iter().enumerate() { + assert!(back_x.equals(&poly.get_coeff_at(i))); + } + + for back_x in back[(max_width / 2)..max_width].iter() { + assert!(back_x.is_zero()); + } + } + } +} diff --git a/constantine/tests/local_tests/mod.rs b/constantine/tests/local_tests/mod.rs new file mode 100644 index 000000000..ef310fa97 --- /dev/null +++ b/constantine/tests/local_tests/mod.rs @@ -0,0 +1,3 @@ +pub mod local_consts; +pub mod local_poly; +pub mod local_recovery; diff --git a/constantine/tests/mod.rs b/constantine/tests/mod.rs new file mode 100644 index 000000000..d1b7273bd --- /dev/null +++ b/constantine/tests/mod.rs @@ -0,0 +1 @@ +pub mod local_tests; diff --git a/constantine/tests/poly.rs b/constantine/tests/poly.rs index 0668ce5f1..9c9de312e 100644 --- a/constantine/tests/poly.rs +++ b/constantine/tests/poly.rs @@ -1,3 +1,6 @@ +// #[path = "./local_tests/local_poly.rs"] +// pub mod local_poly; + #[cfg(test)] mod tests { use kzg_bench::tests::poly::{ @@ -6,77 +9,147 @@ mod tests { poly_inverse_simple_0, poly_inverse_simple_1, poly_mul_direct_test, poly_mul_fft_test, poly_mul_random, poly_test_div, }; - use rust_kzg_zkcrypto::kzg_proofs::FFTSettings; - use rust_kzg_zkcrypto::kzg_types::ZFr; - use rust_kzg_zkcrypto::poly::PolyData; + use rust_kzg_blst::types::fft_settings::FsFFTSettings; + use rust_kzg_blst::types::fr::FsFr; + use rust_kzg_blst::types::poly::FsPoly; + + // Local tests + // #[test] + // fn local_create_poly_of_length_ten_() { + // create_poly_of_length_ten() + // } + // + // #[test] + // fn local_poly_pad_works_rand_() { + // poly_pad_works_rand() + // } + // + // #[test] + // fn local_poly_eval_check_() { + // poly_eval_check() + // } + // + // #[test] + // fn local_poly_eval_0_check_() { poly_eval_0_check() } + // + // #[test] + // fn local_poly_eval_nil_check_() { + // poly_eval_nil_check() + // } + // + // #[test] + // fn local_poly_inverse_simple_0_() { + // poly_inverse_simple_0() + // } + // + // #[test] + // fn local_poly_inverse_simple_1_() { + // poly_inverse_simple_1() + // } + // + // #[test] + // fn local_test_poly_div_by_zero_() { + // test_poly_div_by_zero() + // } + // + // #[test] + // fn local_poly_div_long_test_() { + // poly_div_long_test() + // } + // + // #[test] + // fn local_poly_div_fast_test_() { + // poly_div_fast_test() + // } + // + // #[test] + // fn local_poly_mul_direct_test_() { + // poly_mul_direct_test() + // } + // + // #[test] + // fn local_poly_mul_fft_test_() { + // poly_mul_fft_test() + // } + // + // #[test] + // fn local_poly_mul_random_() { + // poly_mul_random() + // } + // + // #[test] + // fn local_poly_div_random_() { + // poly_div_random() + // } + // Shared tests #[test] fn create_poly_of_length_ten_() { - create_poly_of_length_ten::(); + create_poly_of_length_ten::() } #[test] fn poly_eval_check_() { - poly_eval_check::(); + poly_eval_check::() } #[test] fn poly_eval_0_check_() { - poly_eval_0_check::(); + poly_eval_0_check::() } #[test] fn poly_eval_nil_check_() { - poly_eval_nil_check::(); + poly_eval_nil_check::() } #[test] fn poly_inverse_simple_0_() { - poly_inverse_simple_0::(); + poly_inverse_simple_0::() } #[test] fn poly_inverse_simple_1_() { - poly_inverse_simple_1::(); + poly_inverse_simple_1::() } #[test] fn poly_test_div_() { - poly_test_div::(); + poly_test_div::() } #[test] fn poly_div_by_zero_() { - poly_div_by_zero::(); + poly_div_by_zero::() } #[test] fn poly_mul_direct_test_() { - poly_mul_direct_test::(); + poly_mul_direct_test::() } #[test] fn poly_mul_fft_test_() { - poly_mul_fft_test::(); + poly_mul_fft_test::() } #[test] fn poly_mul_random_() { - poly_mul_random::(); + poly_mul_random::() } #[test] fn poly_div_random_() { - poly_div_random::(); + poly_div_random::() } #[test] fn poly_div_long_test_() { - poly_div_long_test::() + poly_div_long_test::() } #[test] fn poly_div_fast_test_() { - poly_div_fast_test::() + poly_div_fast_test::() } } diff --git a/constantine/tests/recover.rs b/constantine/tests/recover.rs deleted file mode 100644 index 865849ea9..000000000 --- a/constantine/tests/recover.rs +++ /dev/null @@ -1,23 +0,0 @@ -#[cfg(test)] -mod recover_tests { - use kzg_bench::tests::recover::*; - use rust_kzg_zkcrypto::kzg_proofs::FFTSettings; - use rust_kzg_zkcrypto::kzg_types::ZFr as Fr; - use rust_kzg_zkcrypto::poly::PolyData; - - #[test] - fn recover_simple_() { - recover_simple::(); - } - - //Could be not working because of zero poly. - #[test] - fn recover_random_() { - recover_random::(); - } - - #[test] - fn more_than_half_missing_() { - more_than_half_missing::(); - } -} diff --git a/constantine/tests/recovery.rs b/constantine/tests/recovery.rs new file mode 100644 index 000000000..c36668a14 --- /dev/null +++ b/constantine/tests/recovery.rs @@ -0,0 +1,29 @@ +// #[path = "./local_tests/local_recovery.rs"] +// pub mod local_recovery; + +#[cfg(test)] +mod tests { + use kzg_bench::tests::recover::*; + // uncomment to use the local tests + //use crate::local_recovery::{recover_random, recover_simple}; + + use rust_kzg_blst::types::fft_settings::FsFFTSettings; + use rust_kzg_blst::types::fr::FsFr; + use rust_kzg_blst::types::poly::FsPoly; + + // Shared tests + #[test] + fn recover_simple_() { + recover_simple::(); + } + + #[test] + fn recover_random_() { + recover_random::(); + } + + #[test] + fn more_than_half_missing_() { + more_than_half_missing::(); + } +} diff --git a/constantine/tests/zero_poly.rs b/constantine/tests/zero_poly.rs index cab56ecf5..67cded13a 100644 --- a/constantine/tests/zero_poly.rs +++ b/constantine/tests/zero_poly.rs @@ -4,42 +4,42 @@ mod tests { check_test_data, reduce_partials_random, test_reduce_partials, zero_poly_252, zero_poly_all_but_one, zero_poly_known, zero_poly_random, }; - use rust_kzg_zkcrypto::kzg_proofs::FFTSettings; - use rust_kzg_zkcrypto::kzg_types::ZFr; - use rust_kzg_zkcrypto::poly::PolyData; + use rust_kzg_blst::types::fft_settings::FsFFTSettings; + use rust_kzg_blst::types::fr::FsFr; + use rust_kzg_blst::types::poly::FsPoly; #[test] fn test_reduce_partials_() { - test_reduce_partials::(); + test_reduce_partials::(); } #[test] fn reduce_partials_random_() { - reduce_partials_random::(); + reduce_partials_random::(); } #[test] fn check_test_data_() { - check_test_data::(); + check_test_data::(); } #[test] fn zero_poly_known_() { - zero_poly_known::(); + zero_poly_known::(); } #[test] fn zero_poly_random_() { - zero_poly_random::(); + zero_poly_random::(); } #[test] fn zero_poly_all_but_one_() { - zero_poly_all_but_one::(); + zero_poly_all_but_one::(); } #[test] fn zero_poly_252_() { - zero_poly_252::(); + zero_poly_252::(); } } From 792c5a4d238904be4b260053835f3d079959ea3b Mon Sep 17 00:00:00 2001 From: Armantidas Date: Thu, 28 Dec 2023 16:42:08 +0200 Subject: [PATCH 03/17] Finish constantine base --- Cargo.lock | 19 +- constantine/Cargo.toml | 21 +- constantine/benches/das.rs | 4 +- constantine/benches/eip_4844.rs | 2 +- constantine/benches/fft.rs | 6 +- constantine/benches/fk_20.rs | 18 +- constantine/benches/kzg.rs | 14 +- constantine/benches/lincomb.rs | 6 +- constantine/benches/poly.rs | 4 +- constantine/benches/recover.rs | 2 +- constantine/benches/zero_poly.rs | 2 +- constantine/src/consts.rs | 39 ++- constantine/src/data_availability_sampling.rs | 2 +- constantine/src/eip_4844.rs | 20 +- constantine/src/kzg_proofs.rs | 130 ++++----- constantine/src/types/fp.rs | 84 ++++++ constantine/src/types/fr.rs | 106 +++---- constantine/src/types/g1.rs | 259 ++++++++++++++++-- constantine/src/types/g2.rs | 116 ++++++-- constantine/src/types/mod.rs | 1 + constantine/tests/batch_adder.rs | 74 +++++ constantine/tests/bls12_381.rs | 48 ++-- constantine/tests/c_bindings.rs | 2 +- constantine/tests/consts.rs | 16 +- constantine/tests/das.rs | 8 +- constantine/tests/eip_4844.rs | 172 ++++++------ constantine/tests/fft_fr.rs | 14 +- constantine/tests/fft_g1.rs | 20 +- constantine/tests/fk20_proofs.rs | 90 +++--- constantine/tests/kzg_proofs.rs | 75 +---- constantine/tests/local_tests/local_poly.rs | 104 +++---- constantine/tests/poly.rs | 34 +-- constantine/tests/recovery.rs | 12 +- constantine/tests/zero_poly.rs | 20 +- kzg-bench/src/lib.rs | 8 + 35 files changed, 1000 insertions(+), 552 deletions(-) create mode 100644 constantine/src/types/fp.rs create mode 100644 constantine/tests/batch_adder.rs diff --git a/Cargo.lock b/Cargo.lock index 02021ad69..1b37936ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -394,10 +394,24 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" +[[package]] +name = "constantine-core" +version = "0.1.0" +dependencies = [ + "constantine-sys", +] + +[[package]] +name = "constantine-ethereum-kzg" +version = "0.1.0" +dependencies = [ + "constantine-core", + "constantine-sys", +] + [[package]] name = "constantine-sys" version = "0.1.0" -source = "git+https://github.com/mratsim/constantine.git?rev=0afccb412e2b0d2a182d13d1f42f6528307802ae#0afccb412e2b0d2a182d13d1f42f6528307802ae" [[package]] name = "cpufeatures" @@ -1215,6 +1229,9 @@ dependencies = [ name = "rust-kzg-constantine" version = "0.1.0" dependencies = [ + "blst", + "constantine-core", + "constantine-ethereum-kzg", "constantine-sys", "criterion 0.5.1", "hex", diff --git a/constantine/Cargo.toml b/constantine/Cargo.toml index 1d77e6d67..281eb210b 100644 --- a/constantine/Cargo.toml +++ b/constantine/Cargo.toml @@ -4,10 +4,13 @@ version = "0.1.0" edition = "2021" [dependencies] +blst = { git = 'https://github.com/supranational/blst.git' } kzg = { path = "../kzg", default-features = false } libc = { version = "0.2.148", default-features = false } once_cell = { version = "1.18.0", features = ["critical-section"], default-features = false } -constantine-sys = { 'git' = 'https://github.com/mratsim/constantine.git', rev = "0afccb412e2b0d2a182d13d1f42f6528307802ae" } +constantine-ethereum-kzg = { path = "../../constantine/constantine-rust/constantine-ethereum-kzg" } +constantine-sys = { path = "../../constantine/constantine-rust/constantine-sys" } +constantine-core = { path = "../../constantine/constantine-rust/constantine-core" } rand = { version = "0.8.5", optional = true } rayon = { version = "1.8.0", optional = true } smallvec = { version = "1.11.1", features = ["const_generics"] } @@ -22,6 +25,7 @@ rand = "0.8.5" default = [ "std", "rand", + "constantine_msm" ] std = [ "hex/std", @@ -37,6 +41,13 @@ parallel = [ "dep:rayon", "kzg/parallel" ] +constantine_msm = [] +bgmw = [ + "kzg/bgmw" +] +arkmsm = [ + "kzg/arkmsm" +] [[bench]] name = "das" @@ -70,6 +81,14 @@ harness = false name = "eip_4844" harness = false +[[bench]] +name = "eip_4844_constantine" +harness = false + +[[bench]] +name = "eip_4844_constantine_no_conv" +harness = false + [[bench]] name = "lincomb" harness = false diff --git a/constantine/benches/das.rs b/constantine/benches/das.rs index c88352846..bb5f7a171 100644 --- a/constantine/benches/das.rs +++ b/constantine/benches/das.rs @@ -1,7 +1,7 @@ use criterion::{criterion_group, criterion_main, Criterion}; use kzg_bench::benches::das::bench_das_extension; -use rust_kzg_blst::types::fft_settings::CtFFTSettings; -use rust_kzg_blst::types::fr::CtFr; +use rust_kzg_constantine::types::fft_settings::CtFFTSettings; +use rust_kzg_constantine::types::fr::CtFr; fn bench_das_extension_(c: &mut Criterion) { bench_das_extension::(c) diff --git a/constantine/benches/eip_4844.rs b/constantine/benches/eip_4844.rs index a33c00754..1979fa418 100644 --- a/constantine/benches/eip_4844.rs +++ b/constantine/benches/eip_4844.rs @@ -5,7 +5,7 @@ use kzg::eip_4844::{ verify_kzg_proof_rust, }; use kzg_bench::benches::eip_4844::bench_eip_4844; -use rust_kzg_blst::{ +use rust_kzg_constantine::{ eip_4844::load_trusted_setup_filename_rust, types::{ fft_settings::CtFFTSettings, fr::CtFr, g1::CtG1, g2::CtG2, kzg_settings::CtKZGSettings, diff --git a/constantine/benches/fft.rs b/constantine/benches/fft.rs index 0f1231394..d6ab270c6 100644 --- a/constantine/benches/fft.rs +++ b/constantine/benches/fft.rs @@ -1,8 +1,8 @@ use criterion::{criterion_group, criterion_main, Criterion}; use kzg_bench::benches::fft::{bench_fft_fr, bench_fft_g1}; -use rust_kzg_blst::types::fft_settings::CtFFTSettings; -use rust_kzg_blst::types::fr::CtFr; -use rust_kzg_blst::types::g1::CtG1; +use rust_kzg_constantine::types::fft_settings::CtFFTSettings; +use rust_kzg_constantine::types::fr::CtFr; +use rust_kzg_constantine::types::g1::CtG1; fn bench_fft_fr_(c: &mut Criterion) { bench_fft_fr::(c); diff --git a/constantine/benches/fk_20.rs b/constantine/benches/fk_20.rs index 11db1f73e..df65bfcb1 100644 --- a/constantine/benches/fk_20.rs +++ b/constantine/benches/fk_20.rs @@ -1,15 +1,15 @@ use criterion::{criterion_group, criterion_main, Criterion}; use kzg_bench::benches::fk20::{bench_fk_multi_da, bench_fk_single_da}; -use rust_kzg_blst::types::fft_settings::CtFFTSettings; -use rust_kzg_blst::types::fk20_multi_settings::CtFK20MultiSettings; -use rust_kzg_blst::types::fk20_single_settings::CtFK20SingleSettings; -use rust_kzg_blst::types::fr::CtFr; -use rust_kzg_blst::types::g1::CtG1; -use rust_kzg_blst::types::g2::CtG2; -use rust_kzg_blst::types::kzg_settings::CtKZGSettings; -use rust_kzg_blst::types::poly::CtPoly; -use rust_kzg_blst::utils::generate_trusted_setup; +use rust_kzg_constantine::types::fft_settings::CtFFTSettings; +use rust_kzg_constantine::types::fk20_multi_settings::CtFK20MultiSettings; +use rust_kzg_constantine::types::fk20_single_settings::CtFK20SingleSettings; +use rust_kzg_constantine::types::fr::CtFr; +use rust_kzg_constantine::types::g1::CtG1; +use rust_kzg_constantine::types::g2::CtG2; +use rust_kzg_constantine::types::kzg_settings::CtKZGSettings; +use rust_kzg_constantine::types::poly::CtPoly; +use rust_kzg_constantine::utils::generate_trusted_setup; fn bench_fk_single_da_(c: &mut Criterion) { bench_fk_single_da::( diff --git a/constantine/benches/kzg.rs b/constantine/benches/kzg.rs index e9d39ebbc..fd1273eb5 100644 --- a/constantine/benches/kzg.rs +++ b/constantine/benches/kzg.rs @@ -1,12 +1,12 @@ use criterion::{criterion_group, criterion_main, Criterion}; use kzg_bench::benches::kzg::{bench_commit_to_poly, bench_compute_proof_single}; -use rust_kzg_blst::types::fft_settings::CtFFTSettings; -use rust_kzg_blst::types::fr::CtFr; -use rust_kzg_blst::types::g1::CtG1; -use rust_kzg_blst::types::g2::CtG2; -use rust_kzg_blst::types::kzg_settings::CtKZGSettings; -use rust_kzg_blst::types::poly::CtPoly; -use rust_kzg_blst::utils::generate_trusted_setup; +use rust_kzg_constantine::types::fft_settings::CtFFTSettings; +use rust_kzg_constantine::types::fr::CtFr; +use rust_kzg_constantine::types::g1::CtG1; +use rust_kzg_constantine::types::g2::CtG2; +use rust_kzg_constantine::types::kzg_settings::CtKZGSettings; +use rust_kzg_constantine::types::poly::CtPoly; +use rust_kzg_constantine::utils::generate_trusted_setup; fn bench_commit_to_poly_(c: &mut Criterion) { bench_commit_to_poly::( diff --git a/constantine/benches/lincomb.rs b/constantine/benches/lincomb.rs index d020b0dae..ca9b1024d 100644 --- a/constantine/benches/lincomb.rs +++ b/constantine/benches/lincomb.rs @@ -1,8 +1,8 @@ use criterion::{criterion_group, criterion_main, Criterion}; use kzg_bench::benches::lincomb::bench_g1_lincomb; -use rust_kzg_blst::kzg_prooCt::g1_linear_combination; -use rust_kzg_blst::types::fr::CtFr; -use rust_kzg_blst::types::g1::CtG1; +use rust_kzg_constantine::kzg_proofs::g1_linear_combination; +use rust_kzg_constantine::types::fr::CtFr; +use rust_kzg_constantine::types::g1::CtG1; fn bench_g1_lincomb_(c: &mut Criterion) { bench_g1_lincomb::(c, &g1_linear_combination); diff --git a/constantine/benches/poly.rs b/constantine/benches/poly.rs index 7bd31115f..dbbe8101f 100644 --- a/constantine/benches/poly.rs +++ b/constantine/benches/poly.rs @@ -1,7 +1,7 @@ use criterion::{criterion_group, criterion_main, Criterion}; use kzg_bench::benches::poly::bench_new_poly_div; -use rust_kzg_blst::types::fr::CtFr; -use rust_kzg_blst::types::poly::CtPoly; +use rust_kzg_constantine::types::fr::CtFr; +use rust_kzg_constantine::types::poly::CtPoly; fn bench_new_poly_div_(c: &mut Criterion) { bench_new_poly_div::(c); diff --git a/constantine/benches/recover.rs b/constantine/benches/recover.rs index 86a6359af..e02e52019 100644 --- a/constantine/benches/recover.rs +++ b/constantine/benches/recover.rs @@ -1,6 +1,6 @@ use criterion::{criterion_group, criterion_main, Criterion}; use kzg_bench::benches::recover::bench_recover; -use rust_kzg_blst::types::{fft_settings::CtFFTSettings, fr::CtFr, poly::CtPoly}; +use rust_kzg_constantine::types::{fft_settings::CtFFTSettings, fr::CtFr, poly::CtPoly}; pub fn bench_recover_(c: &mut Criterion) { bench_recover::(c) diff --git a/constantine/benches/zero_poly.rs b/constantine/benches/zero_poly.rs index 09ba9c2f1..eb7d59d45 100644 --- a/constantine/benches/zero_poly.rs +++ b/constantine/benches/zero_poly.rs @@ -1,6 +1,6 @@ use criterion::{criterion_group, criterion_main, Criterion}; use kzg_bench::benches::zero_poly::bench_zero_poly; -use rust_kzg_blst::types::{fft_settings::CtFFTSettings, fr::CtFr, poly::CtPoly}; +use rust_kzg_constantine::types::{fft_settings::CtFFTSettings, fr::CtFr, poly::CtPoly}; fn bench_zero_poly_(c: &mut Criterion) { bench_zero_poly::(c); diff --git a/constantine/src/consts.rs b/constantine/src/consts.rs index 17d3af1f2..755a09897 100644 --- a/constantine/src/consts.rs +++ b/constantine/src/consts.rs @@ -1,10 +1,16 @@ -//blst_fp = bls12_381_fp, FsG1 = CtG1, blst_p1 = bls12_381_g1_jac, blst_fr = bls12_381_fr -use constantine_sys::{bls12_381_fp, bls12_381_fp2, bls12_381_g1_jac, bls12_381_g2_jac}; +//blst_fp = bls12_381_fp, CtG1 = CtG1, blst_p1 = bls12_381_g1_jac, blst_fr = bls12_381_fr +use constantine_sys::{ + bls12_381_fp, bls12_381_fp2, bls12_381_g1_aff, bls12_381_g1_jac, bls12_381_g2_jac, +}; use crate::types::g1::CtG1; use crate::types::g2::CtG2; -pub const G1_IDENTITY: CtG1 = CtG1::from_xyz(bls12_381_fp { limbs: [0; 6] },bls12_381_fp { limbs: [0; 6] }, bls12_381_fp { limbs: [0; 6] }); +pub const G1_IDENTITY: CtG1 = CtG1::from_xyz( + bls12_381_fp { limbs: [0; 6] }, + bls12_381_fp { limbs: [0; 6] }, + bls12_381_fp { limbs: [0; 6] }, +); pub const SCALE_FACTOR: u64 = 5; @@ -46,6 +52,29 @@ pub const SCALE2_ROOT_OF_UNITY: [[u64; 4]; 32] = [ [0x63e7cb4906ffc93f, 0xf070bb00e28a193d, 0xad1715b02e5713b5, 0x4b5371495990693f] ]; +// pub const G1_GENERATOR_AFFINE: bls12_381_g1_aff = bls12_381_g1_aff { +// x: bls12_381_fp { +// limbs: [ +// 0x17f1d3a73197d794, +// 0x2695638c4fa9ac0f, +// 0xc3688c4f9774b905, +// 0xa14e3a3f171bac58, +// 0x6c55e83ff97a1aef, +// 0xfb3af00adb22c6bb, +// ], +// }, +// y: bls12_381_fp { +// limbs: [ +// 0xbaac93d50ce72271, +// 0x8c22631a7918fd8e, +// 0xdd595f13570725ce, +// 0x51ac582950405194, +// 0x0e1c8c3fad0059c0, +// 0x0bbc3efc5008a26a, +// ], +// }, +// }); + pub const G1_GENERATOR: CtG1 = CtG1(bls12_381_g1_jac { x: bls12_381_fp { limbs: [ @@ -213,7 +242,7 @@ pub const G2_NEGATIVE_GENERATOR: CtG2 = CtG2(bls12_381_g2_jac { ], }, y: bls12_381_fp2 { - fp: [ + c: [ bls12_381_fp { limbs: [ 0x6d8bf5079fb65e61, @@ -237,7 +266,7 @@ pub const G2_NEGATIVE_GENERATOR: CtG2 = CtG2(bls12_381_g2_jac { ], }, z: bls12_381_fp2 { - fp: [ + c: [ bls12_381_fp { limbs: [ 0x760900000002fffd, diff --git a/constantine/src/data_availability_sampling.rs b/constantine/src/data_availability_sampling.rs index f2134e580..afad1efbe 100644 --- a/constantine/src/data_availability_sampling.rs +++ b/constantine/src/data_availability_sampling.rs @@ -1,4 +1,4 @@ -//blst_fp = bls12_381_fp, FsG1 = CtG1, blst_p1 = bls12_381_g1_jacbls12_381_g1_jac +//blst_fp = bls12_381_fp, CtG1 = CtG1, blst_p1 = bls12_381_g1_jacbls12_381_g1_jac extern crate alloc; use alloc::string::String; diff --git a/constantine/src/eip_4844.rs b/constantine/src/eip_4844.rs index b8e67c52b..5e7eec72e 100644 --- a/constantine/src/eip_4844.rs +++ b/constantine/src/eip_4844.rs @@ -34,7 +34,7 @@ use crate::types::g1::CtG1; use crate::types::g2::CtG2; use crate::types::kzg_settings::CtKZGSettings; -use constantine_sys::{bls12_381_g1_jac, bls12_381_g2_jac, bls12_381_fr}; +use constantine_sys::{bls12_381_fr, bls12_381_g1_jac, bls12_381_g2_jac}; #[cfg(feature = "parallel")] use rayon::prelude::*; @@ -56,7 +56,7 @@ fn fft_settings_to_rust(c_settings: *const CKZGSettings) -> Result>() }; let mut expanded_roots_of_unity = roots_of_unity.clone(); @@ -82,7 +82,7 @@ fn kzg_settings_to_rust(c_settings: &CKZGSettings) -> Result>() }; Ok(CtKZGSettings { @@ -91,7 +91,7 @@ fn kzg_settings_to_rust(c_settings: &CKZGSettings) -> Result>() }, }) @@ -101,14 +101,14 @@ fn kzg_settings_to_c(rust_settings: &CtKZGSettings) -> CKZGSettings { let g1_val = rust_settings .secret_g1 .iter() - .map(|r| r.0) - .collect::>(); + .map(|r| r.to_blst_p1()) + .collect::>(); let g1_val = Box::new(g1_val); let g2_val = rust_settings .secret_g2 .iter() - .map(|r| r.0) - .collect::>(); + .map(|r| r.to_blst_p2()) + .collect::>(); let x = g2_val.into_boxed_slice(); let stat_ref = Box::leak(x); let v = Box::into_raw(g1_val); @@ -118,8 +118,8 @@ fn kzg_settings_to_c(rust_settings: &CtKZGSettings) -> CKZGSettings { .fs .roots_of_unity .iter() - .map(|r| r.0) - .collect::>(), + .map(|r| r.to_blst_fr()) + .collect::>(), ); CKZGSettings { diff --git a/constantine/src/kzg_proofs.rs b/constantine/src/kzg_proofs.rs index a75490e9e..14dbba05b 100644 --- a/constantine/src/kzg_proofs.rs +++ b/constantine/src/kzg_proofs.rs @@ -1,30 +1,25 @@ extern crate alloc; -#[cfg(not(feature = "parallel"))] -use alloc::vec; -use alloc::vec::Vec; -#[cfg(not(feature = "parallel"))] -use core::ptr; - -/*#[cfg(feature = "parallel")] -use blst::p1_affines; -#[cfg(not(feature = "parallel"))] -use blst::{ - blst_p1s_mult_pippenger, blst_p1s_mult_pippenger_scratch_sizeof, blst_p1s_to_affine, limb_t, -}; +use crate::types::fp::CtFp; +use crate::types::g1::CtG1; +use crate::types::{fr::CtFr, g1::CtG1Affine}; -use blst::{ - blst_fp12_is_one, blst_p1, blst_p1_affine, blst_p1_cneg, blst_p1_to_affine, blst_p2_affine, - blst_p2_to_affine, blst_scalar, blst_scalar_from_fr, Pairing, -};*/ +use crate::types::g1::CtG1ProjAddAffine; -use kzg::{G1Mul, PairingVerify, G1}; +use constantine_sys as constantine; +// use constantine_ethereum_kzg::Threadpool as constantine_pool; + +use kzg::G1Affine; + +use kzg::msm::msm_impls::msm; -use crate::types::fr::CtFr; -use crate::types::g1::CtG1; use crate::types::g2::CtG2; +use blst::{ + blst_fp12_is_one, blst_p1_affine, blst_p1_cneg, blst_p1_to_affine, blst_p2_affine, + blst_p2_to_affine, Pairing, +}; -use constantine_sys::{bls12_381_g1_aff, bls12_381_g1_jac, bls12_381_g2_aff, ctt_bls12_381_g1_jac_cneg_in_place}; +use kzg::PairingVerify; impl PairingVerify for CtG1 { fn verify(a1: &CtG1, a2: &CtG2, b1: &CtG1, b2: &CtG2) -> bool { @@ -33,93 +28,64 @@ impl PairingVerify for CtG1 { } pub fn g1_linear_combination(out: &mut CtG1, points: &[CtG1], scalars: &[CtFr], len: usize) { - if len < 8 { - *out = CtG1::default(); - for i in 0..len { - let tmp = points[i].mul(&scalars[i]); - *out = out.add_or_dbl(&tmp); - } - return; - } - - #[cfg(feature = "parallel")] - { - let points = unsafe { core::slice::from_raw_parts(points.as_ptr() as *const blst_p1, len) }; - let points = p1_affines::from(points); - - let mut scalar_bytes: Vec = Vec::with_capacity(len * 32); - for bytes in scalars.iter().map(|b| { - let mut scalar = blst_scalar::default(); - - unsafe { blst_scalar_from_fr(&mut scalar, &b.0) } - - scalar.b - }) { - scalar_bytes.extend_from_slice(&bytes); - } - - let res = points.mult(scalar_bytes.as_slice(), 255); - *out = CtG1(res) - } - - #[cfg(not(feature = "parallel"))] + #[cfg(feature = "constantine_msm")] { - let mut scratch: Vec; - unsafe { - scratch = vec![0u8; blst_p1s_mult_pippenger_scratch_sizeof(len)]; - } + #[cfg(feature = "parallel")] + let pool = + unsafe { constantine::ctt_threadpool_new(constantine_sys::ctt_cpu_get_num_threads_os()) }; - let mut p_affine = vec![bls12_381_g1_aff::default(); len]; - let mut p_scalars = vec![blst_scalar::default(); len]; + #[cfg(not(feature = "parallel"))] + let pool = unsafe { constantine::ctt_threadpool_new(1) }; - let p_arg: [*const bls12_381_g1_jac; 2] = [&points[0].0, ptr::null()]; unsafe { - blst_p1s_to_affine(p_affine.as_mut_ptr(), p_arg.as_ptr(), len); - } + let points_affine_vec = CtG1Affine::into_affines(points); + let points_transmuted: &[constantine::bls12_381_g1_aff] = + core::mem::transmute(points_affine_vec.as_slice()); - for i in 0..len { - unsafe { blst_scalar_from_fr(&mut p_scalars[i], &scalars[i].0) }; - } - - let scalars_arg: [*const blst_scalar; 2] = [p_scalars.as_ptr(), ptr::null()]; - let points_arg: [*const bls12_381_g1_aff; 2] = [p_affine.as_ptr(), ptr::null()]; - unsafe { - blst_p1s_mult_pippenger( + let frs_transmuted: &[constantine::bls12_381_fr] = core::mem::transmute(scalars); + constantine::ctt_bls12_381_g1_jac_multi_scalar_mul_fr_coefs_vartime_parallel( + pool, &mut out.0, - points_arg.as_ptr(), + frs_transmuted.as_ptr(), + points_transmuted.as_ptr(), len, - scalars_arg.as_ptr() as *const *const u8, - 255, - scratch.as_mut_ptr() as *mut limb_t, ); } + + unsafe { + constantine::ctt_threadpool_shutdown(pool); + } } + + #[cfg(not(feature = "constantine_msm"))] + *out = msm::(points, scalars, len); } pub fn pairings_verify(a1: &CtG1, a2: &CtG2, b1: &CtG1, b2: &CtG2) -> bool { - let mut aa1 = bls12_381_g1_aff::default(); - let mut bb1 = bls12_381_g1_aff::default(); + // FIXME: Remove usage of BLST version, though not sure if there's a constantine version of multi miller loop + let mut aa1 = blst_p1_affine::default(); + let mut bb1 = blst_p1_affine::default(); - let mut aa2 = bls12_381_g2_aff::default(); - let mut bb2 = bls12_381_g2_aff::default(); + let mut aa2 = blst_p2_affine::default(); + let mut bb2 = blst_p2_affine::default(); // As an optimisation, we want to invert one of the pairings, // so we negate one of the points. let mut a1neg: CtG1 = *a1; unsafe { - ctt_bls12_381_g1_jac_cneg_in_place(&mut a1neg.0, true); - blst_p1_to_affine(&mut aa1, &a1neg.0); + blst_p1_cneg(core::mem::transmute(&mut a1neg.0), true); + blst_p1_to_affine(&mut aa1, core::mem::transmute(&a1neg.0)); - blst_p1_to_affine(&mut bb1, &b1.0); - blst_p2_to_affine(&mut aa2, &a2.0); - blst_p2_to_affine(&mut bb2, &b2.0); + blst_p1_to_affine(&mut bb1, core::mem::transmute(&b1.0)); + blst_p2_to_affine(&mut aa2, core::mem::transmute(&a2.0)); + blst_p2_to_affine(&mut bb2, core::mem::transmute(&b2.0)); let dst = [0u8; 3]; - let mut pairing_blst = Pairing::new(false, &dst); + let mut pairing_blst = blst::Pairing::new(false, &dst); pairing_blst.raw_aggregate(&aa2, &aa1); pairing_blst.raw_aggregate(&bb2, &bb1); let gt_point = pairing_blst.as_fp12().final_exp(); - blst_fp12_is_one(>_point) + blst::blst_fp12_is_one(>_point) } } diff --git a/constantine/src/types/fp.rs b/constantine/src/types/fp.rs new file mode 100644 index 000000000..58cdbe55a --- /dev/null +++ b/constantine/src/types/fp.rs @@ -0,0 +1,84 @@ +use constantine_sys as constantine; +use constantine_sys::bls12_381_fp; +use kzg::G1Fp; + +#[repr(C)] +#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)] +pub struct CtFp(pub bls12_381_fp); +impl G1Fp for CtFp { + const ONE: Self = Self(bls12_381_fp { + limbs: [ + 8505329371266088957, + 17002214543764226050, + 6865905132761471162, + 8632934651105793861, + 6631298214892334189, + 1582556514881692819, + ], + }); + const ZERO: Self = Self(bls12_381_fp { + limbs: [0, 0, 0, 0, 0, 0], + }); + const BLS12_381_RX_P: Self = Self(bls12_381_fp { + limbs: [ + 8505329371266088957, + 17002214543764226050, + 6865905132761471162, + 8632934651105793861, + 6631298214892334189, + 1582556514881692819, + ], + }); + + fn inverse(&self) -> Option { + let mut out: Self = *self; + unsafe { + constantine::ctt_bls12_381_fp_inv(&mut out.0, &self.0); + } + Some(out) + } + + fn square(&self) -> Self { + let mut out: Self = Default::default(); + unsafe { + constantine::ctt_bls12_381_fp_square(&mut out.0, &self.0); + } + out + } + + fn double(&self) -> Self { + let mut out: Self = Default::default(); + unsafe { + constantine::ctt_bls12_381_fp_double(&mut out.0, &self.0); + } + out + } + + fn from_underlying_arr(arr: &[u64; 6]) -> Self { + Self(bls12_381_fp { limbs: *arr }) + } + + fn neg_assign(&mut self) { + unsafe { + constantine::ctt_bls12_381_fp_neg_in_place(&mut self.0); + } + } + + fn mul_assign_fp(&mut self, b: &Self) { + unsafe { + constantine::ctt_bls12_381_fp_mul_in_place(&mut self.0, &b.0); + } + } + + fn sub_assign_fp(&mut self, b: &Self) { + unsafe { + constantine::ctt_bls12_381_fp_sub_in_place(&mut self.0, &b.0); + } + } + + fn add_assign_fp(&mut self, b: &Self) { + unsafe { + constantine::ctt_bls12_381_fp_add_in_place(&mut self.0, &b.0); + } + } +} diff --git a/constantine/src/types/fr.rs b/constantine/src/types/fr.rs index d9100df32..4e4a4a42f 100644 --- a/constantine/src/types/fr.rs +++ b/constantine/src/types/fr.rs @@ -1,20 +1,36 @@ -//blst_fp = bls12_381_fp, FsG1 = CtG1, blst_p1 = bls12_381_g1_jac, blst_fr = bls12_381_fr +//blst_fp = bls12_381_fp, CtG1 = CtG1, blst_p1 = bls12_381_g1_jac, blst_fr = bls12_381_fr extern crate alloc; use alloc::format; use alloc::string::String; use alloc::string::ToString; - +use blst::blst_fr; use kzg::eip_4844::BYTES_PER_FIELD_ELEMENT; use kzg::Fr; +use kzg::Scalar256; + +use constantine_sys as constantine; -use constantine_sys::{bls12_381_fr, ctt_bls12_381_fr_square, ctt_bls12_381_fr_sum, ctt_bls12_381_fr_diff, ctt_bls12_381_fr_prod, ctt_bls12_381_fr_inv, - ctt_bls12_381_fr_cneg_in_place}; +use constantine_sys::{ + bls12_381_fr, ctt_bls12_381_fr_cneg_in_place, ctt_bls12_381_fr_diff, ctt_bls12_381_fr_inv, + ctt_bls12_381_fr_prod, ctt_bls12_381_fr_square, ctt_bls12_381_fr_sum, +}; -#[derive(Debug, Clone, Copy, Eq, PartialEq, Default)] +#[repr(C)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub struct CtFr(pub bls12_381_fr); +impl CtFr { + pub fn from_blst_fr(fr: blst::blst_fr) -> Self { + Self(bls12_381_fr { limbs: fr.l }) + } + + pub fn to_blst_fr(&self) -> blst_fr { + blst_fr { l: self.0.limbs } + } +} + impl Fr for CtFr { fn null() -> Self { Self::from_u64_arr(&[u64::MAX, u64::MAX, u64::MAX, u64::MAX]) @@ -38,7 +54,7 @@ impl Fr for CtFr { ]; let mut ret = Self::default(); unsafe { - blst_fr_from_uint64(&mut ret.0, val.as_ptr()); + blst::blst_fr_from_uint64(core::mem::transmute(&mut ret.0), val.as_ptr()); } ret @@ -55,16 +71,17 @@ impl Fr for CtFr { ) }) .and_then(|bytes: &[u8; BYTES_PER_FIELD_ELEMENT]| { - let mut bls_scalar = blst_scalar::default(); - let mut fr = bls12_381_fr::default(); + let mut ret: Self = Self::default(); + let mut bls_scalar = blst::blst_scalar::default(); unsafe { - blst_scalar_from_bendian(&mut bls_scalar, bytes.as_ptr()); - if !blst_scalar_fr_check(&bls_scalar) { + // FIXME: Change to constantine version when available + blst::blst_scalar_from_bendian(&mut bls_scalar, bytes.as_ptr()); + if !blst::blst_scalar_fr_check(&bls_scalar) { return Err("Invalid scalar".to_string()); } - blst_fr_from_scalar(&mut fr, &bls_scalar); + blst::blst_fr_from_scalar(core::mem::transmute(&mut ret.0), &bls_scalar); } - Ok(Self(fr)) + Ok(ret) }) } @@ -79,13 +96,13 @@ impl Fr for CtFr { ) }) .map(|bytes: &[u8; BYTES_PER_FIELD_ELEMENT]| { - let mut bls_scalar = blst_scalar::default(); - let mut fr = bls12_381_fr::default(); + let mut ret = Self::default(); + let mut bls_scalar = blst::blst_scalar::default(); unsafe { - blst_scalar_from_bendian(&mut bls_scalar, bytes.as_ptr()); - blst_fr_from_scalar(&mut fr, &bls_scalar); + blst::blst_scalar_from_bendian(&mut bls_scalar, bytes.as_ptr()); + blst::blst_fr_from_scalar(core::mem::transmute(&mut ret.0), &bls_scalar); } - Self(fr) + ret }) } @@ -97,7 +114,7 @@ impl Fr for CtFr { fn from_u64_arr(u: &[u64; 4]) -> Self { let mut ret = Self::default(); unsafe { - blst_fr_from_uint64(&mut ret.0, u.as_ptr()); + blst::blst_fr_from_uint64(core::mem::transmute(&mut ret.0), u.as_ptr()); } ret @@ -108,11 +125,11 @@ impl Fr for CtFr { } fn to_bytes(&self) -> [u8; 32] { - let mut scalar = blst_scalar::default(); + let mut scalar = blst::blst_scalar::default(); let mut bytes = [0u8; 32]; unsafe { - blst_scalar_from_fr(&mut scalar, &self.0); - blst_bendian_from_scalar(bytes.as_mut_ptr(), &scalar); + blst::blst_scalar_from_fr(&mut scalar, core::mem::transmute(&self.0)); + blst::blst_bendian_from_scalar(bytes.as_mut_ptr(), &scalar); } bytes @@ -121,28 +138,18 @@ impl Fr for CtFr { fn to_u64_arr(&self) -> [u64; 4] { let mut val: [u64; 4] = [0; 4]; unsafe { - blst_uint64_from_fr(val.as_mut_ptr(), &self.0); + blst::blst_uint64_from_fr(val.as_mut_ptr(), core::mem::transmute(&self.0)); } val } fn is_one(&self) -> bool { - let mut val: [u64; 4] = [0; 4]; - unsafe { - blst_uint64_from_fr(val.as_mut_ptr(), &self.0); - } - - val[0] == 1 && val[1] == 0 && val[2] == 0 && val[3] == 0 + unsafe { constantine::ctt_bls12_381_fr_is_one(&self.0) != 0 } } fn is_zero(&self) -> bool { - let mut val: [u64; 4] = [0; 4]; - unsafe { - blst_uint64_from_fr(val.as_mut_ptr(), &self.0); - } - - val[0] == 0 && val[1] == 0 && val[2] == 0 && val[3] == 0 + unsafe { constantine::ctt_bls12_381_fr_is_zero(&self.0) != 0 } } fn is_null(&self) -> bool { @@ -151,9 +158,7 @@ impl Fr for CtFr { fn sqr(&self) -> Self { let mut ret = Self::default(); - unsafe { - ctt_bls12_381_fr_square(&mut ret.0, &self.0) - } + unsafe { constantine::ctt_bls12_381_fr_square(&mut ret.0, &self.0) } ret } @@ -161,7 +166,7 @@ impl Fr for CtFr { fn mul(&self, b: &Self) -> Self { let mut ret = Self::default(); unsafe { - ctt_bls12_381_fr_prod(&mut ret.0, &self.0, &b.0); + constantine::ctt_bls12_381_fr_prod(&mut ret.0, &self.0, &b.0); } ret @@ -170,7 +175,7 @@ impl Fr for CtFr { fn add(&self, b: &Self) -> Self { let mut ret = Self::default(); unsafe { - ctt_bls12_381_fr_sum(&mut ret.0, &self.0, &b.0); + constantine::ctt_bls12_381_fr_sum(&mut ret.0, &self.0, &b.0); } ret @@ -179,7 +184,7 @@ impl Fr for CtFr { fn sub(&self, b: &Self) -> Self { let mut ret = Self::default(); unsafe { - ctt_bls12_381_fr_diff(&mut ret.0, &self.0, &b.0); + constantine::ctt_bls12_381_fr_diff(&mut ret.0, &self.0, &b.0); } ret @@ -188,16 +193,16 @@ impl Fr for CtFr { fn eucl_inverse(&self) -> Self { let mut ret = Self::default(); unsafe { - ctt_bls12_381_fr_inv(&mut ret.0, &self.0); + constantine::ctt_bls12_381_fr_inv(&mut ret.0, &self.0); } ret } fn negate(&self) -> Self { - let mut ret = Self::default(); + let mut ret = *self; unsafe { - ctt_bls12_381_fr_cneg_in_place(&mut ret.0, &self.0, true); + constantine::ctt_bls12_381_fr_neg_in_place(&mut ret.0); } ret @@ -206,7 +211,7 @@ impl Fr for CtFr { fn inverse(&self) -> Self { let mut ret = Self::default(); unsafe { - ctt_bls12_381_fr_inv(&mut ret.0, &self.0); + constantine::ctt_bls12_381_fr_inv(&mut ret.0, &self.0); } ret @@ -244,10 +249,19 @@ impl Fr for CtFr { let mut val_b: [u64; 4] = [0; 4]; unsafe { - blst_uint64_from_fr(val_a.as_mut_ptr(), &self.0); - blst_uint64_from_fr(val_b.as_mut_ptr(), &b.0); + constantine::ctt_bls12_381_fr_marshalBE(val_a.as_mut_ptr() as *mut u8, 32, &self.0); + constantine::ctt_bls12_381_fr_marshalBE(val_b.as_mut_ptr() as *mut u8, 32, &b.0); } val_a[0] == val_b[0] && val_a[1] == val_b[1] && val_a[2] == val_b[2] && val_a[3] == val_b[3] } + + fn to_scalar(&self) -> kzg::Scalar256 { + // FIXME: Change to constantine version when available + let mut blst_scalar = blst::blst_scalar::default(); + unsafe { + blst::blst_scalar_from_fr(&mut blst_scalar, core::mem::transmute(&self.0)); + } + Scalar256::from_u8(&blst_scalar.b) + } } diff --git a/constantine/src/types/g1.rs b/constantine/src/types/g1.rs index 4f3983768..4aff74a16 100644 --- a/constantine/src/types/g1.rs +++ b/constantine/src/types/g1.rs @@ -4,25 +4,56 @@ use alloc::format; use alloc::string::String; use alloc::string::ToString; +use crate::types::fp::CtFp; +use crate::types::fr::CtFr; use kzg::common_utils::log_2_byte; use kzg::eip_4844::BYTES_PER_G1; +use kzg::G1Affine; +use kzg::G1GetFp; +use kzg::G1ProjAddAffine; use kzg::{G1Mul, G1}; use crate::consts::{G1_GENERATOR, G1_IDENTITY, G1_NEGATIVE_GENERATOR}; -use crate::kzg_proofs::g1_linear_combination; -use crate::types::fr::CtFr; +// use crate::kzg_proofs::g1_linear_combination; -use constantine_sys::{bls12_381_fp, bls12_381_g1_jac, bls12_381_g1_aff, ctt_bls12_381_g1_jac_double, ctt_bls12_381_g1_jac_sum, ctt_bls12_381_g1_jac_is_inf, - ctt_bls12_381_g1_jac_is_eq, ctt_bls12_381_g1_jac_cneg_in_place, ctt_bls12_381_g1_jac_from_affine}; +use constantine_sys as constantine; + +use constantine_sys::{ + bls12_381_fp, bls12_381_g1_aff, bls12_381_g1_jac, ctt_bls12_381_g1_jac_cneg_in_place, + ctt_bls12_381_g1_jac_double, ctt_bls12_381_g1_jac_from_affine, ctt_bls12_381_g1_jac_is_eq, + ctt_bls12_381_g1_jac_is_inf, ctt_bls12_381_g1_jac_sum, +}; #[repr(C)] -#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)] +#[derive(Debug, Clone, Copy, Default, Eq)] pub struct CtG1(pub bls12_381_g1_jac); +impl PartialEq for CtG1 { + fn eq(&self, other: &Self) -> bool { + unsafe { constantine::ctt_bls12_381_g1_jac_is_eq(&self.0, &other.0) != 0 } + } +} + impl CtG1 { pub(crate) const fn from_xyz(x: bls12_381_fp, y: bls12_381_fp, z: bls12_381_fp) -> Self { CtG1(bls12_381_g1_jac { x, y, z }) } + + pub const fn from_blst_p1(p1: blst::blst_p1) -> Self { + Self(bls12_381_g1_jac { + x: bls12_381_fp { limbs: p1.x.l }, + y: bls12_381_fp { limbs: p1.y.l }, + z: bls12_381_fp { limbs: p1.z.l }, + }) + } + + pub const fn to_blst_p1(&self) -> blst::blst_p1 { + blst::blst_p1 { + x: blst::blst_fp { l: self.0.x.limbs }, + y: blst::blst_fp { l: self.0.y.limbs }, + z: blst::blst_fp { l: self.0.z.limbs }, + } + } } impl G1 for CtG1 { @@ -58,8 +89,11 @@ impl G1 for CtG1 { let mut tmp = bls12_381_g1_aff::default(); let mut g1 = bls12_381_g1_jac::default(); unsafe { + let tmp_ref: &mut blst::blst_p1_affine = core::mem::transmute(&mut tmp); // The uncompress routine also checks that the point is on the curve - if blst_p1_uncompress(&mut tmp, bytes.as_ptr()) != BLST_ERROR::BLST_SUCCESS { + if blst::blst_p1_uncompress(tmp_ref, bytes.as_ptr()) + != blst::BLST_ERROR::BLST_SUCCESS + { return Err("Failed to uncompress".to_string()); } ctt_bls12_381_g1_jac_from_affine(&mut g1, &tmp); @@ -76,34 +110,35 @@ impl G1 for CtG1 { fn to_bytes(&self) -> [u8; 48] { let mut out = [0u8; BYTES_PER_G1]; unsafe { - blst_p1_compress(out.as_mut_ptr(), &self.0); + let inp_ref: &blst::blst_p1 = core::mem::transmute(&self.0); + blst::blst_p1_compress(out.as_mut_ptr(), inp_ref); } out } - fn add_or_dbl(&mut self, b: &Self) -> Self { + fn add_or_dbl(&self, b: &Self) -> Self { let mut ret = Self::default(); unsafe { - blst_p1_add_or_double(&mut ret.0, &self.0, &b.0); + constantine::ctt_bls12_381_g1_jac_sum(&mut ret.0, &self.0, &b.0); } ret } fn is_inf(&self) -> bool { - unsafe { ctt_bls12_381_g1_jac_is_inf(&self.0) } + unsafe { constantine::ctt_bls12_381_g1_jac_is_inf(&self.0) != 0 } } fn is_valid(&self) -> bool { unsafe { - // The point must be on the right subgroup - blst_p1_in_g1(&self.0) + // FIXME: Constantine equivalent + blst::blst_p1_in_g1(core::mem::transmute(&self.0)) } } fn dbl(&self) -> Self { let mut result = bls12_381_g1_jac::default(); unsafe { - ctt_bls12_381_g1_jac_double(&mut result, &self.0); + constantine::ctt_bls12_381_g1_jac_double(&mut result, &self.0); } Self(result) } @@ -111,31 +146,55 @@ impl G1 for CtG1 { fn add(&self, b: &Self) -> Self { let mut ret = Self::default(); unsafe { - ctt_bls12_381_g1_jac_sum(&mut ret.0, &self.0, &b.0); + constantine::ctt_bls12_381_g1_jac_sum(&mut ret.0, &self.0, &b.0); } ret } fn sub(&self, b: &Self) -> Self { - let mut b_negative: CtG1 = *b; let mut ret = Self::default(); unsafe { - ctt_bls12_381_g1_jac_cneg_in_place(&mut b_negative.0, true); - blst_p1_add_or_double(&mut ret.0, &self.0, &b_negative.0); - ret + constantine::ctt_bls12_381_g1_jac_diff(&mut ret.0, &self.0, &b.0); } + ret } fn equals(&self, b: &Self) -> bool { - unsafe { ctt_bls12_381_g1_jac_is_eq(&self.0, &b.0) } + unsafe { constantine::ctt_bls12_381_g1_jac_is_eq(&self.0, &b.0) != 0 } + } + + // FIXME: Wrong here + const ZERO: Self = CtG1::from_xyz( + bls12_381_fp { limbs: [0; 6] }, + bls12_381_fp { limbs: [0; 6] }, + bls12_381_fp { limbs: [0; 6] }, + ); + + fn add_or_dbl_assign(&mut self, b: &Self) { + unsafe { + constantine::ctt_bls12_381_g1_jac_add_in_place(&mut self.0, &b.0); + } + } + + fn add_assign(&mut self, b: &Self) { + unsafe { + constantine::ctt_bls12_381_g1_jac_add_in_place(&mut self.0, &b.0); + } + } + + fn dbl_assign(&mut self) { + unsafe { + constantine::ctt_bls12_381_g1_jac_double_in_place(&mut self.0); + } } } impl G1Mul for CtG1 { fn mul(&self, b: &CtFr) -> Self { - let mut scalar: blst_scalar = blst_scalar::default(); + // FIXME: No transmute here, use constantine + let mut scalar = blst::blst_scalar::default(); unsafe { - blst_scalar_from_fr(&mut scalar, &b.0); + blst::blst_scalar_from_fr(&mut scalar, core::mem::transmute(&b.0)); } // Count the number of bytes to be multiplied. @@ -152,9 +211,9 @@ impl G1Mul for CtG1 { } else { // Count the number of bits to be multiplied. unsafe { - blst_p1_mult( - &mut result.0, - &self.0, + blst::blst_p1_mult( + core::mem::transmute(&mut result.0), + core::mem::transmute(&self.0), &(scalar.b[0]), 8 * i - 7 + log_2_byte(scalar.b[i - 1]), ); @@ -165,7 +224,157 @@ impl G1Mul for CtG1 { fn g1_lincomb(points: &[Self], scalars: &[CtFr], len: usize) -> Self { let mut out = CtG1::default(); - g1_linear_combination(&mut out, points, scalars, len); + crate::kzg_proofs::g1_linear_combination(&mut out, points, scalars, len); out } } + +impl G1GetFp for CtG1 { + fn x(&self) -> &CtFp { + unsafe { + // Transmute safe due to repr(C) on CtFp + core::mem::transmute(&self.0.x) + } + } + + fn y(&self) -> &CtFp { + unsafe { + // Transmute safe due to repr(C) on CtFp + core::mem::transmute(&self.0.y) + } + } + + fn z(&self) -> &CtFp { + unsafe { + // Transmute safe due to repr(C) on CtFp + core::mem::transmute(&self.0.z) + } + } + + fn x_mut(&mut self) -> &mut CtFp { + unsafe { + // Transmute safe due to repr(C) on CtFp + core::mem::transmute(&mut self.0.x) + } + } + + fn y_mut(&mut self) -> &mut CtFp { + unsafe { + // Transmute safe due to repr(C) on CtFp + core::mem::transmute(&mut self.0.y) + } + } + + fn z_mut(&mut self) -> &mut CtFp { + unsafe { + // Transmute safe due to repr(C) on CtFp + core::mem::transmute(&mut self.0.z) + } + } +} + +#[repr(C)] +#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)] +pub struct CtG1Affine(pub constantine::bls12_381_g1_aff); + +impl G1Affine for CtG1Affine { + const ZERO: Self = Self(bls12_381_g1_aff { + x: { + bls12_381_fp { + limbs: [0, 0, 0, 0, 0, 0], + } + }, + y: { + bls12_381_fp { + limbs: [0, 0, 0, 0, 0, 0], + } + }, + }); + + fn into_affine(g1: &CtG1) -> Self { + let mut ret: Self = Default::default(); + unsafe { + constantine::ctt_bls12_381_g1_jac_affine(&mut ret.0, &g1.0); + } + ret + } + + fn into_affines_loc(out: &mut [Self], g1: &[CtG1]) { + g1.iter() + .zip(out.iter_mut()) + .for_each(|(g, out_slot)| unsafe { + constantine::ctt_bls12_381_g1_jac_affine(&mut out_slot.0, &g.0); + }); + } + + fn into_affines(g1: &[CtG1]) -> Vec { + g1.iter() + .map(|g| { + let mut ret = Self::default(); + unsafe { + constantine::ctt_bls12_381_g1_jac_affine(&mut ret.0, &g.0); + } + ret + }) + .collect::>() + } + + fn to_proj(&self) -> CtG1 { + let mut ret: CtG1 = Default::default(); + unsafe { + constantine::ctt_bls12_381_g1_jac_from_affine(&mut ret.0, &self.0); + } + ret + } + + fn x(&self) -> &CtFp { + unsafe { + // Transmute safe due to repr(C) on CtFp + core::mem::transmute(&self.0.x) + } + } + + fn y(&self) -> &CtFp { + unsafe { + // Transmute safe due to repr(C) on CtFp + core::mem::transmute(&self.0.y) + } + } + + fn is_infinity(&self) -> bool { + unsafe { constantine::ctt_bls12_381_g1_aff_is_inf(&self.0) != 0 } + } + + fn x_mut(&mut self) -> &mut CtFp { + unsafe { + // Transmute safe due to repr(C) on CtFp + core::mem::transmute(&mut self.0.x) + } + } + + fn y_mut(&mut self) -> &mut CtFp { + unsafe { + // Transmute safe due to repr(C) on CtFp + core::mem::transmute(&mut self.0.y) + } + } +} + +pub struct CtG1ProjAddAffine; +impl G1ProjAddAffine for CtG1ProjAddAffine { + fn add_assign_affine(proj: &mut CtG1, aff: &CtG1Affine) { + let mut g1_jac = bls12_381_g1_jac::default(); + unsafe { + constantine::ctt_bls12_381_g1_jac_from_affine(&mut g1_jac, &aff.0); + constantine::ctt_bls12_381_g1_jac_add_in_place(&mut proj.0, &g1_jac); + } + } + + fn add_or_double_assign_affine(proj: &mut CtG1, aff: &CtG1Affine) { + let mut g1_jac = bls12_381_g1_jac::default(); + unsafe { + constantine::ctt_bls12_381_g1_jac_from_affine(&mut g1_jac, &aff.0); + constantine::ctt_bls12_381_g1_jac_add_in_place(&mut proj.0, &g1_jac); + } + } +} diff --git a/constantine/src/types/g2.rs b/constantine/src/types/g2.rs index 100914ff7..e412da30a 100644 --- a/constantine/src/types/g2.rs +++ b/constantine/src/types/g2.rs @@ -12,23 +12,100 @@ use kzg::{G2Mul, G2}; use crate::consts::{G2_GENERATOR, G2_NEGATIVE_GENERATOR}; use crate::types::fr::CtFr; -use constantine_sys::{bls12_381_g2_jac, bls12_381_g2_aff, ctt_bls12_381_g2_jac_cneg_in_place, bls12_381_fp2, ctt_bls12_381_fp2_double_in_place, - ctt_bls12_381_g2_jac_from_affine, ctt_bls12_381_g1_jac_is_eq}; +use constantine_sys::{ + bls12_381_fp, bls12_381_fp2, bls12_381_g2_aff, bls12_381_g2_jac, + ctt_bls12_381_fp2_double_in_place, ctt_bls12_381_g1_jac_is_eq, + ctt_bls12_381_g2_jac_cneg_in_place, ctt_bls12_381_g2_jac_from_affine, +}; + +use constantine_sys as constantine; #[derive(Debug, Default, Clone, Copy, Eq, PartialEq)] pub struct CtG2(pub bls12_381_g2_jac); +impl CtG2 { + pub const fn from_blst_p2(p2: blst::blst_p2) -> Self { + Self(bls12_381_g2_jac { + x: bls12_381_fp2 { + c: [ + bls12_381_fp { + limbs: p2.x.fp[0].l, + }, + bls12_381_fp { + limbs: p2.x.fp[1].l, + }, + ], + }, + y: bls12_381_fp2 { + c: [ + bls12_381_fp { + limbs: p2.y.fp[0].l, + }, + bls12_381_fp { + limbs: p2.y.fp[1].l, + }, + ], + }, + z: bls12_381_fp2 { + c: [ + bls12_381_fp { + limbs: p2.z.fp[0].l, + }, + bls12_381_fp { + limbs: p2.z.fp[1].l, + }, + ], + }, + }) + } + + pub const fn to_blst_p2(&self) -> blst::blst_p2 { + blst::blst_p2 { + x: blst::blst_fp2 { + fp: [ + blst::blst_fp { + l: self.0.x.c[0].limbs, + }, + blst::blst_fp { + l: self.0.x.c[1].limbs, + }, + ], + }, + y: blst::blst_fp2 { + fp: [ + blst::blst_fp { + l: self.0.y.c[0].limbs, + }, + blst::blst_fp { + l: self.0.y.c[1].limbs, + }, + ], + }, + z: blst::blst_fp2 { + fp: [ + blst::blst_fp { + l: self.0.z.c[0].limbs, + }, + blst::blst_fp { + l: self.0.z.c[1].limbs, + }, + ], + }, + } + } +} + impl G2Mul for CtG2 { fn mul(&self, b: &CtFr) -> Self { let mut result = bls12_381_g2_jac::default(); - let mut scalar = blst_scalar::default(); + let mut scalar = blst::blst_scalar::default(); unsafe { - blst_scalar_from_fr(&mut scalar, &b.0); - blst_p2_mult( - &mut result, - &self.0, + blst::blst_scalar_from_fr(&mut scalar, core::mem::transmute(&b.0)); + blst::blst_p2_mult( + core::mem::transmute(&mut result), + core::mem::transmute(&self.0), scalar.b.as_ptr(), - 8 * core::mem::size_of::(), + 8 * core::mem::size_of::(), ); } Self(result) @@ -58,8 +135,11 @@ impl G2 for CtG2 { let mut tmp = bls12_381_g2_aff::default(); let mut g2 = bls12_381_g2_jac::default(); unsafe { + let tmp_ref: &mut blst::blst_p2_affine = core::mem::transmute(&mut tmp); // The uncompress routine also checks that the point is on the curve - if blst_p2_uncompress(&mut tmp, bytes.as_ptr()) != BLST_ERROR::BLST_SUCCESS { + if blst::blst_p2_uncompress(tmp_ref, bytes.as_ptr()) + != blst::BLST_ERROR::BLST_SUCCESS + { return Err("Failed to uncompress".to_string()); } ctt_bls12_381_g2_jac_from_affine(&mut g2, &tmp); @@ -71,15 +151,16 @@ impl G2 for CtG2 { fn to_bytes(&self) -> [u8; 96] { let mut out = [0u8; BYTES_PER_G2]; unsafe { - blst_p2_compress(out.as_mut_ptr(), &self.0); + let inp_ref: &blst::blst_p2 = core::mem::transmute(&self.0); + blst::blst_p2_compress(out.as_mut_ptr(), inp_ref); } out } fn add_or_dbl(&mut self, b: &Self) -> Self { - let mut result = bls12_381_g2_jac::default(); + let mut result = self.0; unsafe { - blst_p2_add_or_double(&mut result, &self.0, &b.0); + constantine::ctt_bls12_381_g2_jac_add_in_place(&mut result, &b.0); } Self(result) } @@ -87,24 +168,23 @@ impl G2 for CtG2 { fn dbl(&self) -> Self { let mut result = bls12_381_g2_jac::default(); unsafe { - ctt_bls12_381_fp2_double_in_place(&mut result); + constantine::ctt_bls12_381_g2_jac_double(&mut result, &self.0); } Self(result) } fn sub(&self, b: &Self) -> Self { let mut bneg: bls12_381_g2_jac = b.0; - let mut result = bls12_381_g2_jac::default(); + let mut result = self.0; unsafe { - //blst_p2_cneg(&mut bneg, true); - ctt_bls12_381_g2_jac_cneg_in_place(&mut bneg, true); - blst_p2_add_or_double(&mut result, &self.0, &bneg); + constantine::ctt_bls12_381_g2_jac_neg_in_place(&mut bneg); + constantine::ctt_bls12_381_g2_jac_add_in_place(&mut result, &bneg); } Self(result) } fn equals(&self, b: &Self) -> bool { - unsafe { ctt_bls12_381_g1_jac_is_eq(&self.0, &b.0) } + unsafe { constantine::ctt_bls12_381_g2_jac_is_eq(&self.0, &b.0) != 0 } } } diff --git a/constantine/src/types/mod.rs b/constantine/src/types/mod.rs index 97961af26..3be18fd91 100644 --- a/constantine/src/types/mod.rs +++ b/constantine/src/types/mod.rs @@ -1,6 +1,7 @@ pub mod fft_settings; pub mod fk20_multi_settings; pub mod fk20_single_settings; +pub mod fp; pub mod fr; pub mod g1; pub mod g2; diff --git a/constantine/tests/batch_adder.rs b/constantine/tests/batch_adder.rs new file mode 100644 index 000000000..46a1e3b94 --- /dev/null +++ b/constantine/tests/batch_adder.rs @@ -0,0 +1,74 @@ +#[cfg(test)] +mod tests { + use kzg_bench::tests::msm::batch_adder::{ + test_batch_add, test_batch_add_indexed, test_batch_add_indexed_single_bucket, + test_batch_add_step_n, test_phase_one_p_add_p, test_phase_one_p_add_q, + test_phase_one_p_add_q_twice, test_phase_one_zero_or_neg, test_phase_two_p_add_neg, + test_phase_two_p_add_p, test_phase_two_p_add_q, test_phase_two_zero_add_p, + }; + use rust_kzg_constantine::types::{ + fp::CtFp, + g1::{CtG1, CtG1Affine}, + }; + // use rust_kzg_blst::types:: + + #[test] + fn test_phase_one_zero_or_neg_() { + test_phase_one_zero_or_neg::(); + } + + #[test] + fn test_phase_one_p_add_p_() { + test_phase_one_p_add_p::(); + } + + #[test] + fn test_phase_one_p_add_q_() { + test_phase_one_p_add_q::(); + } + + #[test] + fn test_phase_one_p_add_q_twice_() { + test_phase_one_p_add_q_twice::(); + } + + #[test] + fn test_phase_two_zero_add_p_() { + test_phase_two_zero_add_p::(); + } + + #[test] + fn test_phase_two_p_add_neg_() { + test_phase_two_p_add_neg::(); + } + + #[test] + fn test_phase_two_p_add_q_() { + test_phase_two_p_add_q::(); + } + + #[test] + fn test_phase_two_p_add_p_() { + test_phase_two_p_add_p::(); + } + + #[test] + fn test_batch_add_() { + test_batch_add::(); + } + + #[test] + fn test_batch_add_step_n_() { + test_batch_add_step_n::(); + } + + #[test] + fn test_batch_add_indexed_() { + test_batch_add_indexed::(); + } + + #[test] + fn test_batch_add_indexed_single_bucket_() { + test_batch_add_indexed_single_bucket::(); + } +} diff --git a/constantine/tests/bls12_381.rs b/constantine/tests/bls12_381.rs index ca5be831e..816c6e334 100644 --- a/constantine/tests/bls12_381.rs +++ b/constantine/tests/bls12_381.rs @@ -9,10 +9,10 @@ mod tests { p2_add_or_dbl_works, p2_mul_works, p2_sub_works, pairings_work, }; - use rust_kzg_blst::kzg_proofs::{g1_linear_combination, pairings_verify}; - use rust_kzg_blst::types::fr::FsFr; - use rust_kzg_blst::types::g1::FsG1; - use rust_kzg_blst::types::g2::FsG2; + use rust_kzg_constantine::kzg_proofs::{g1_linear_combination, pairings_verify}; + use rust_kzg_constantine::types::fr::CtFr; + use rust_kzg_constantine::types::g1::CtG1; + use rust_kzg_constantine::types::g2::CtG2; #[test] fn log_2_byte_works_() { @@ -21,101 +21,101 @@ mod tests { #[test] fn fr_is_null_works_() { - fr_is_null_works::() + fr_is_null_works::() } #[test] fn fr_is_zero_works_() { - fr_is_zero_works::() + fr_is_zero_works::() } #[test] fn fr_is_one_works_() { - fr_is_one_works::() + fr_is_one_works::() } #[test] fn fr_from_uint64_works_() { - fr_from_uint64_works::() + fr_from_uint64_works::() } #[test] fn fr_equal_works_() { - fr_equal_works::() + fr_equal_works::() } #[test] fn fr_negate_works_() { - fr_negate_works::() + fr_negate_works::() } #[test] fn fr_pow_works_() { - fr_pow_works::() + fr_pow_works::() } #[test] fn fr_div_works_() { - fr_div_works::() + fr_div_works::() } #[test] fn fr_div_by_zero_() { - fr_div_by_zero::() + fr_div_by_zero::() } #[test] fn fr_uint64s_roundtrip_() { - fr_uint64s_roundtrip::() + fr_uint64s_roundtrip::() } #[test] fn p1_mul_works_() { - p1_mul_works::() + p1_mul_works::() } #[test] fn p1_sub_works_() { - p1_sub_works::() + p1_sub_works::() } #[test] fn p2_add_or_dbl_works_() { - p2_add_or_dbl_works::() + p2_add_or_dbl_works::() } #[test] fn p2_mul_works_() { - p2_mul_works::() + p2_mul_works::() } #[test] fn p2_sub_works_() { - p2_sub_works::() + p2_sub_works::() } #[test] fn g1_identity_is_infinity_() { - g1_identity_is_infinity::() + g1_identity_is_infinity::() } #[test] fn g1_identity_is_identity_() { - g1_identity_is_identity::() + g1_identity_is_identity::() } #[test] fn g1_make_linear_combination_() { - g1_make_linear_combination::(&g1_linear_combination) + g1_make_linear_combination::(&g1_linear_combination) } #[test] fn g1_random_linear_combination_() { - g1_random_linear_combination::(&g1_linear_combination) + g1_random_linear_combination::(&g1_linear_combination) } #[test] fn pairings_work_() { - pairings_work::(&pairings_verify) + pairings_work::(&pairings_verify) } } diff --git a/constantine/tests/c_bindings.rs b/constantine/tests/c_bindings.rs index e08446a4e..0aead562b 100644 --- a/constantine/tests/c_bindings.rs +++ b/constantine/tests/c_bindings.rs @@ -10,7 +10,7 @@ mod tests { load_trusted_setup_invalid_g1_point_test, load_trusted_setup_invalid_g2_byte_length_test, load_trusted_setup_invalid_g2_point_test, }; - use rust_kzg_blst::eip_4844::{ + use rust_kzg_constantine::eip_4844::{ blob_to_kzg_commitment, compute_blob_kzg_proof, free_trusted_setup, load_trusted_setup, load_trusted_setup_file, }; diff --git a/constantine/tests/consts.rs b/constantine/tests/consts.rs index e3b11b86d..94c650269 100644 --- a/constantine/tests/consts.rs +++ b/constantine/tests/consts.rs @@ -7,9 +7,9 @@ mod tests { expand_roots_is_plausible, new_fft_settings_is_plausible, roots_of_unity_are_plausible, roots_of_unity_is_the_expected_size, roots_of_unity_out_of_bounds_fails, }; - use rust_kzg_blst::consts::SCALE2_ROOT_OF_UNITY; - use rust_kzg_blst::types::fft_settings::{expand_root_of_unity, FsFFTSettings}; - use rust_kzg_blst::types::fr::FsFr; + use rust_kzg_constantine::consts::SCALE2_ROOT_OF_UNITY; + use rust_kzg_constantine::types::fft_settings::{expand_root_of_unity, CtFFTSettings}; + use rust_kzg_constantine::types::fr::CtFr; // Shared tests #[test] @@ -19,27 +19,27 @@ mod tests { #[test] fn roots_of_unity_out_of_bounds_fails_() { - roots_of_unity_out_of_bounds_fails::(); + roots_of_unity_out_of_bounds_fails::(); } #[test] fn roots_of_unity_are_plausible_() { - roots_of_unity_are_plausible::(&SCALE2_ROOT_OF_UNITY); + roots_of_unity_are_plausible::(&SCALE2_ROOT_OF_UNITY); } #[test] fn expand_roots_is_plausible_() { - expand_roots_is_plausible::(&SCALE2_ROOT_OF_UNITY, &expand_root_of_unity); + expand_roots_is_plausible::(&SCALE2_ROOT_OF_UNITY, &expand_root_of_unity); } #[test] fn new_fft_settings_is_plausible_() { - new_fft_settings_is_plausible::(); + new_fft_settings_is_plausible::(); } // Local tests // #[test] // fn roots_of_unity_repeat_at_stride_() { - // roots_of_unity_repeat_at_stride::(); + // roots_of_unity_repeat_at_stride::(); // } } diff --git a/constantine/tests/das.rs b/constantine/tests/das.rs index 40e32ddf3..f799af7a0 100644 --- a/constantine/tests/das.rs +++ b/constantine/tests/das.rs @@ -1,16 +1,16 @@ #[cfg(test)] mod tests { use kzg_bench::tests::das::{das_extension_test_known, das_extension_test_random}; - use rust_kzg_blst::types::fft_settings::FsFFTSettings; - use rust_kzg_blst::types::fr::FsFr; + use rust_kzg_constantine::types::fft_settings::CtFFTSettings; + use rust_kzg_constantine::types::fr::CtFr; #[test] fn das_extension_test_known_() { - das_extension_test_known::(); + das_extension_test_known::(); } #[test] fn das_extension_test_random_() { - das_extension_test_random::(); + das_extension_test_random::(); } } diff --git a/constantine/tests/eip_4844.rs b/constantine/tests/eip_4844.rs index cc2e5cd3c..4ec77904a 100644 --- a/constantine/tests/eip_4844.rs +++ b/constantine/tests/eip_4844.rs @@ -24,27 +24,27 @@ mod tests { test_vectors_verify_kzg_proof, validate_batched_input_test, verify_kzg_proof_batch_fails_with_incorrect_proof_test, verify_kzg_proof_batch_test, }; - use rust_kzg_blst::consts::SCALE2_ROOT_OF_UNITY; - use rust_kzg_blst::eip_4844::load_trusted_setup_filename_rust; - use rust_kzg_blst::types::fft_settings::expand_root_of_unity; - use rust_kzg_blst::types::{ - fft_settings::FsFFTSettings, fr::FsFr, g1::FsG1, g2::FsG2, kzg_settings::FsKZGSettings, - poly::FsPoly, + use rust_kzg_constantine::consts::SCALE2_ROOT_OF_UNITY; + use rust_kzg_constantine::eip_4844::load_trusted_setup_filename_rust; + use rust_kzg_constantine::types::fft_settings::expand_root_of_unity; + use rust_kzg_constantine::types::{ + fft_settings::CtFFTSettings, fr::CtFr, g1::CtG1, g2::CtG2, kzg_settings::CtKZGSettings, + poly::CtPoly, }; #[test] pub fn bytes_to_bls_field_test_() { - bytes_to_bls_field_test::(); + bytes_to_bls_field_test::(); } #[test] pub fn compute_powers_test_() { - compute_powers_test::(&compute_powers); + compute_powers_test::(&compute_powers); } #[test] pub fn blob_to_kzg_commitment_test_() { - blob_to_kzg_commitment_test::( + blob_to_kzg_commitment_test::( &load_trusted_setup_filename_rust, &blob_to_kzg_commitment_rust, ); @@ -52,7 +52,7 @@ mod tests { #[test] pub fn compute_kzg_proof_test_() { - compute_kzg_proof_test::( + compute_kzg_proof_test::( &load_trusted_setup_filename_rust, &compute_kzg_proof_rust, &blob_to_polynomial, @@ -63,12 +63,12 @@ mod tests { #[test] pub fn compute_and_verify_kzg_proof_round_trip_test_() { compute_and_verify_kzg_proof_round_trip_test::< - FsFr, - FsG1, - FsG2, - FsPoly, - FsFFTSettings, - FsKZGSettings, + CtFr, + CtG1, + CtG2, + CtPoly, + CtFFTSettings, + CtKZGSettings, >( &load_trusted_setup_filename_rust, &blob_to_kzg_commitment_rust, @@ -83,12 +83,12 @@ mod tests { #[test] pub fn compute_and_verify_kzg_proof_within_domain_test_() { compute_and_verify_kzg_proof_within_domain_test::< - FsFr, - FsG1, - FsG2, - FsPoly, - FsFFTSettings, - FsKZGSettings, + CtFr, + CtG1, + CtG2, + CtPoly, + CtFFTSettings, + CtKZGSettings, >( &load_trusted_setup_filename_rust, &blob_to_kzg_commitment_rust, @@ -103,12 +103,12 @@ mod tests { #[test] pub fn compute_and_verify_kzg_proof_fails_with_incorrect_proof_test_() { compute_and_verify_kzg_proof_fails_with_incorrect_proof_test::< - FsFr, - FsG1, - FsG2, - FsPoly, - FsFFTSettings, - FsKZGSettings, + CtFr, + CtG1, + CtG2, + CtPoly, + CtFFTSettings, + CtKZGSettings, >( &load_trusted_setup_filename_rust, &blob_to_kzg_commitment_rust, @@ -123,12 +123,12 @@ mod tests { #[test] pub fn compute_and_verify_blob_kzg_proof_test_() { compute_and_verify_blob_kzg_proof_test::< - FsFr, - FsG1, - FsG2, - FsPoly, - FsFFTSettings, - FsKZGSettings, + CtFr, + CtG1, + CtG2, + CtPoly, + CtFFTSettings, + CtKZGSettings, >( &load_trusted_setup_filename_rust, &blob_to_kzg_commitment_rust, @@ -141,12 +141,12 @@ mod tests { #[test] pub fn compute_and_verify_blob_kzg_proof_fails_with_incorrect_proof_test_() { compute_and_verify_blob_kzg_proof_fails_with_incorrect_proof_test::< - FsFr, - FsG1, - FsG2, - FsPoly, - FsFFTSettings, - FsKZGSettings, + CtFr, + CtG1, + CtG2, + CtPoly, + CtFFTSettings, + CtKZGSettings, >( &load_trusted_setup_filename_rust, &blob_to_kzg_commitment_rust, @@ -158,7 +158,7 @@ mod tests { #[test] pub fn verify_kzg_proof_batch_test_() { - verify_kzg_proof_batch_test::( + verify_kzg_proof_batch_test::( &load_trusted_setup_filename_rust, &blob_to_kzg_commitment_rust, &bytes_to_blob, @@ -170,12 +170,12 @@ mod tests { #[test] pub fn verify_kzg_proof_batch_fails_with_incorrect_proof_test_() { verify_kzg_proof_batch_fails_with_incorrect_proof_test::< - FsFr, - FsG1, - FsG2, - FsPoly, - FsFFTSettings, - FsKZGSettings, + CtFr, + CtG1, + CtG2, + CtPoly, + CtFFTSettings, + CtKZGSettings, >( &load_trusted_setup_filename_rust, &blob_to_kzg_commitment_rust, @@ -187,7 +187,7 @@ mod tests { #[test] pub fn test_vectors_blob_to_kzg_commitment_() { - test_vectors_blob_to_kzg_commitment::( + test_vectors_blob_to_kzg_commitment::( &load_trusted_setup_filename_rust, &blob_to_kzg_commitment_rust, &bytes_to_blob, @@ -196,7 +196,7 @@ mod tests { #[test] pub fn test_vectors_compute_kzg_proof_() { - test_vectors_compute_kzg_proof::( + test_vectors_compute_kzg_proof::( &load_trusted_setup_filename_rust, &compute_kzg_proof_rust, &bytes_to_blob, @@ -205,7 +205,7 @@ mod tests { #[test] pub fn test_vectors_compute_blob_kzg_proof_() { - test_vectors_compute_blob_kzg_proof::( + test_vectors_compute_blob_kzg_proof::( &load_trusted_setup_filename_rust, &bytes_to_blob, &compute_blob_kzg_proof_rust, @@ -214,7 +214,7 @@ mod tests { #[test] pub fn test_vectors_verify_kzg_proof_() { - test_vectors_verify_kzg_proof::( + test_vectors_verify_kzg_proof::( &load_trusted_setup_filename_rust, &verify_kzg_proof_rust, ); @@ -222,7 +222,7 @@ mod tests { #[test] pub fn test_vectors_verify_blob_kzg_proof_() { - test_vectors_verify_blob_kzg_proof::( + test_vectors_verify_blob_kzg_proof::( &load_trusted_setup_filename_rust, &bytes_to_blob, &verify_blob_kzg_proof_rust, @@ -232,12 +232,12 @@ mod tests { #[test] pub fn test_vectors_verify_blob_kzg_proof_batch_() { test_vectors_verify_blob_kzg_proof_batch::< - FsFr, - FsG1, - FsG2, - FsPoly, - FsFFTSettings, - FsKZGSettings, + CtFr, + CtG1, + CtG2, + CtPoly, + CtFFTSettings, + CtKZGSettings, >( &load_trusted_setup_filename_rust, &bytes_to_blob, @@ -247,72 +247,72 @@ mod tests { #[test] pub fn expand_root_of_unity_too_long() { - let out = expand_root_of_unity(&FsFr::from_u64_arr(&SCALE2_ROOT_OF_UNITY[1]), 1); + let out = expand_root_of_unity(&CtFr::from_u64_arr(&SCALE2_ROOT_OF_UNITY[1]), 1); assert!(out.is_err()); } #[test] pub fn expand_root_of_unity_too_short() { - let out = expand_root_of_unity(&FsFr::from_u64_arr(&SCALE2_ROOT_OF_UNITY[1]), 3); + let out = expand_root_of_unity(&CtFr::from_u64_arr(&SCALE2_ROOT_OF_UNITY[1]), 3); assert!(out.is_err()); } #[test] pub fn compute_kzg_proof_incorrect_blob_length() { - compute_kzg_proof_incorrect_blob_length_test::(&blob_to_polynomial); + compute_kzg_proof_incorrect_blob_length_test::(&blob_to_polynomial); } #[test] pub fn compute_kzg_proof_incorrect_poly_length() { compute_kzg_proof_incorrect_poly_length_test::< - FsPoly, - FsFr, - FsG1, - FsG2, - FsFFTSettings, - FsKZGSettings, + CtPoly, + CtFr, + CtG1, + CtG2, + CtFFTSettings, + CtKZGSettings, >(&evaluate_polynomial_in_evaluation_form); } #[test] pub fn compute_kzg_proof_empty_blob_vector() { compute_kzg_proof_empty_blob_vector_test::< - FsPoly, - FsFr, - FsG1, - FsG2, - FsFFTSettings, - FsKZGSettings, + CtPoly, + CtFr, + CtG1, + CtG2, + CtFFTSettings, + CtKZGSettings, >(&verify_blob_kzg_proof_batch_rust) } #[test] pub fn compute_kzg_proof_incorrect_commitments_len() { compute_kzg_proof_incorrect_commitments_len_test::< - FsPoly, - FsFr, - FsG1, - FsG2, - FsFFTSettings, - FsKZGSettings, + CtPoly, + CtFr, + CtG1, + CtG2, + CtFFTSettings, + CtKZGSettings, >(&verify_blob_kzg_proof_batch_rust) } #[test] pub fn compute_kzg_proof_incorrect_proofs_len() { compute_kzg_proof_incorrect_proofs_len_test::< - FsPoly, - FsFr, - FsG1, - FsG2, - FsFFTSettings, - FsKZGSettings, + CtPoly, + CtFr, + CtG1, + CtG2, + CtFFTSettings, + CtKZGSettings, >(&verify_blob_kzg_proof_batch_rust) } #[test] pub fn validate_batched_input() { - validate_batched_input_test::( + validate_batched_input_test::( &verify_blob_kzg_proof_batch_rust, &load_trusted_setup_filename_rust, ) diff --git a/constantine/tests/fft_fr.rs b/constantine/tests/fft_fr.rs index cfe48acb8..a0ef9bbba 100644 --- a/constantine/tests/fft_fr.rs +++ b/constantine/tests/fft_fr.rs @@ -1,27 +1,27 @@ #[cfg(test)] mod tests { use kzg_bench::tests::fft_fr::{compare_sft_fft, inverse_fft, roundtrip_fft, stride_fft}; - use rust_kzg_blst::fft_fr::{fft_fr_fast, fft_fr_slow}; - use rust_kzg_blst::types::fft_settings::FsFFTSettings; - use rust_kzg_blst::types::fr::FsFr; + use rust_kzg_constantine::fft_fr::{fft_fr_fast, fft_fr_slow}; + use rust_kzg_constantine::types::fft_settings::CtFFTSettings; + use rust_kzg_constantine::types::fr::CtFr; #[test] fn compare_sft_fft_() { - compare_sft_fft::(&fft_fr_slow, &fft_fr_fast); + compare_sft_fft::(&fft_fr_slow, &fft_fr_fast); } #[test] fn roundtrip_fft_() { - roundtrip_fft::(); + roundtrip_fft::(); } #[test] fn inverse_fft_() { - inverse_fft::(); + inverse_fft::(); } #[test] fn stride_fft_() { - stride_fft::(); + stride_fft::(); } } diff --git a/constantine/tests/fft_g1.rs b/constantine/tests/fft_g1.rs index cf0f079aa..09e1b008d 100644 --- a/constantine/tests/fft_g1.rs +++ b/constantine/tests/fft_g1.rs @@ -2,17 +2,17 @@ mod tests { use kzg::G1; use kzg_bench::tests::fft_g1::{compare_ft_fft, roundtrip_fft, stride_fft}; - use rust_kzg_blst::consts::G1_GENERATOR; - use rust_kzg_blst::fft_g1::{fft_g1_fast, fft_g1_slow}; - use rust_kzg_blst::types::fft_settings::FsFFTSettings; - use rust_kzg_blst::types::fr::FsFr; - use rust_kzg_blst::types::g1::FsG1; + use rust_kzg_constantine::consts::G1_GENERATOR; + use rust_kzg_constantine::fft_g1::{fft_g1_fast, fft_g1_slow}; + use rust_kzg_constantine::types::fft_settings::CtFFTSettings; + use rust_kzg_constantine::types::fr::CtFr; + use rust_kzg_constantine::types::g1::CtG1; - fn make_data(n: usize) -> Vec { + fn make_data(n: usize) -> Vec { if n == 0 { return Vec::new(); } - let mut result: Vec = vec![FsG1::default(); n]; + let mut result: Vec = vec![CtG1::default(); n]; result[0] = G1_GENERATOR; for i in 1..n { result[i] = result[i - 1].add_or_dbl(&G1_GENERATOR) @@ -23,16 +23,16 @@ mod tests { #[test] fn roundtrip_fft_() { - roundtrip_fft::(&make_data); + roundtrip_fft::(&make_data); } #[test] fn stride_fft_() { - stride_fft::(&make_data); + stride_fft::(&make_data); } #[test] fn compare_sft_fft_() { - compare_ft_fft::(&fft_g1_slow, &fft_g1_fast, &make_data); + compare_ft_fft::(&fft_g1_slow, &fft_g1_fast, &make_data); } } diff --git a/constantine/tests/fk20_proofs.rs b/constantine/tests/fk20_proofs.rs index bb241aaf3..9bb24e5aa 100644 --- a/constantine/tests/fk20_proofs.rs +++ b/constantine/tests/fk20_proofs.rs @@ -1,19 +1,19 @@ #[cfg(test)] mod tests { use kzg_bench::tests::fk20_proofs::*; - use rust_kzg_blst::types::fft_settings::FsFFTSettings; - use rust_kzg_blst::types::fk20_multi_settings::FsFK20MultiSettings; - use rust_kzg_blst::types::fk20_single_settings::FsFK20SingleSettings; - use rust_kzg_blst::types::fr::FsFr; - use rust_kzg_blst::types::g1::FsG1; - use rust_kzg_blst::types::g2::FsG2; - use rust_kzg_blst::types::kzg_settings::FsKZGSettings; - use rust_kzg_blst::types::poly::FsPoly; - use rust_kzg_blst::utils::generate_trusted_setup; + use rust_kzg_constantine::types::fft_settings::CtFFTSettings; + use rust_kzg_constantine::types::fk20_multi_settings::CtFK20MultiSettings; + use rust_kzg_constantine::types::fk20_single_settings::CtFK20SingleSettings; + use rust_kzg_constantine::types::fr::CtFr; + use rust_kzg_constantine::types::g1::CtG1; + use rust_kzg_constantine::types::g2::CtG2; + use rust_kzg_constantine::types::kzg_settings::CtKZGSettings; + use rust_kzg_constantine::types::poly::CtPoly; + use rust_kzg_constantine::utils::generate_trusted_setup; #[test] fn test_fk_single() { - fk_single::( + fk_single::( &generate_trusted_setup, ); } @@ -21,65 +21,65 @@ mod tests { #[test] fn test_fk_single_strided() { fk_single_strided::< - FsFr, - FsG1, - FsG2, - FsPoly, - FsFFTSettings, - FsKZGSettings, - FsFK20SingleSettings, + CtFr, + CtG1, + CtG2, + CtPoly, + CtFFTSettings, + CtKZGSettings, + CtFK20SingleSettings, >(&generate_trusted_setup); } #[test] fn test_fk_multi_settings() { fk_multi_settings::< - FsFr, - FsG1, - FsG2, - FsPoly, - FsFFTSettings, - FsKZGSettings, - FsFK20MultiSettings, + CtFr, + CtG1, + CtG2, + CtPoly, + CtFFTSettings, + CtKZGSettings, + CtFK20MultiSettings, >(&generate_trusted_setup); } #[test] fn test_fk_multi_chunk_len_1_512() { fk_multi_chunk_len_1_512::< - FsFr, - FsG1, - FsG2, - FsPoly, - FsFFTSettings, - FsKZGSettings, - FsFK20MultiSettings, + CtFr, + CtG1, + CtG2, + CtPoly, + CtFFTSettings, + CtKZGSettings, + CtFK20MultiSettings, >(&generate_trusted_setup); } #[test] fn test_fk_multi_chunk_len_16_512() { fk_multi_chunk_len_16_512::< - FsFr, - FsG1, - FsG2, - FsPoly, - FsFFTSettings, - FsKZGSettings, - FsFK20MultiSettings, + CtFr, + CtG1, + CtG2, + CtPoly, + CtFFTSettings, + CtKZGSettings, + CtFK20MultiSettings, >(&generate_trusted_setup); } #[test] fn test_fk_multi_chunk_len_16_16() { fk_multi_chunk_len_16_16::< - FsFr, - FsG1, - FsG2, - FsPoly, - FsFFTSettings, - FsKZGSettings, - FsFK20MultiSettings, + CtFr, + CtG1, + CtG2, + CtPoly, + CtFFTSettings, + CtKZGSettings, + CtFK20MultiSettings, >(&generate_trusted_setup); } } diff --git a/constantine/tests/kzg_proofs.rs b/constantine/tests/kzg_proofs.rs index 9e0676d1b..eb27fe6bb 100644 --- a/constantine/tests/kzg_proofs.rs +++ b/constantine/tests/kzg_proofs.rs @@ -9,92 +9,39 @@ mod tests { commit_to_nil_poly, commit_to_too_long_poly_returns_err, proof_multi, proof_single, }; - use rust_kzg_blst::types::fft_settings::FsFFTSettings; - use rust_kzg_blst::types::fr::FsFr; - use rust_kzg_blst::types::g1::FsG1; - use rust_kzg_blst::types::g2::FsG2; - use rust_kzg_blst::types::kzg_settings::FsKZGSettings; - use rust_kzg_blst::types::poly::FsPoly; - use rust_kzg_blst::utils::generate_trusted_setup; + use rust_kzg_constantine::types::fft_settings::CtFFTSettings; + use rust_kzg_constantine::types::fr::CtFr; + use rust_kzg_constantine::types::g1::CtG1; + use rust_kzg_constantine::types::g2::CtG2; + use rust_kzg_constantine::types::kzg_settings::CtKZGSettings; + use rust_kzg_constantine::types::poly::CtPoly; + use rust_kzg_constantine::utils::generate_trusted_setup; #[test] pub fn test_proof_single() { - proof_single::( + proof_single::( &generate_trusted_setup, ); } #[test] pub fn test_commit_to_nil_poly() { - commit_to_nil_poly::( + commit_to_nil_poly::( &generate_trusted_setup, ); } #[test] pub fn test_commit_to_too_long_poly() { - commit_to_too_long_poly_returns_err::( + commit_to_too_long_poly_returns_err::( &generate_trusted_setup, ); } #[test] pub fn test_proof_multi() { - proof_multi::( + proof_multi::( &generate_trusted_setup, ); } - - // This aims at showing that the use of the blst::Pairing engine in pairings_verify - // has the desired semantics. - #[cfg(feature = "rand")] - fn og_pairings_verify() { - let a1 = FsG1::rand(); - let a2 = FsG2::rand(); - let b1 = FsG1::rand(); - let b2 = FsG2::rand(); - - let mut loop0 = blst_fp12::default(); - let mut loop1 = blst_fp12::default(); - let mut gt_point = blst_fp12::default(); - - let mut aa1 = blst_p1_affine::default(); - let mut bb1 = blst_p1_affine::default(); - - let mut aa2 = blst_p2_affine::default(); - let mut bb2 = blst_p2_affine::default(); - - // As an optimisation, we want to invert one of the pairings, - // so we negate one of the points. - let mut a1neg: FsG1 = a1; - unsafe { - blst_p1_cneg(&mut a1neg.0, true); - blst_p1_to_affine(&mut aa1, &a1neg.0); - - blst_p1_to_affine(&mut bb1, &b1.0); - blst_p2_to_affine(&mut aa2, &a2.0); - blst_p2_to_affine(&mut bb2, &b2.0); - - blst_miller_loop(&mut loop0, &aa2, &aa1); - blst_miller_loop(&mut loop1, &bb2, &bb1); - - blst_fp12_mul(&mut gt_point, &loop0, &loop1); - blst_final_exp(&mut gt_point, >_point); - - let dst = [0u8; 3]; - let mut pairing_blst = Pairing::new(false, &dst); - pairing_blst.raw_aggregate(&aa2, &aa1); - pairing_blst.raw_aggregate(&bb2, &bb1); - - assert_eq!(gt_point, pairing_blst.as_fp12().final_exp()); - } - } - - #[cfg(feature = "rand")] - #[test] - pub fn test_pairings_verify() { - for _i in 0..100 { - og_pairings_verify(); - } - } } diff --git a/constantine/tests/local_tests/local_poly.rs b/constantine/tests/local_tests/local_poly.rs index 4c4f290ae..2fd10c370 100644 --- a/constantine/tests/local_tests/local_poly.rs +++ b/constantine/tests/local_tests/local_poly.rs @@ -2,11 +2,11 @@ use kzg::{Fr, Poly}; use rand::rngs::StdRng; use rand::{RngCore, SeedableRng}; -use rust_kzg_blst::types::fr::FsFr; -use rust_kzg_blst::types::poly::FsPoly; +use rust_kzg_constantine::types::fr::CtFr; +use rust_kzg_constantine::types::poly::CtPoly; pub fn create_poly_of_length_ten() { - let poly = FsPoly::new(10); + let poly = CtPoly::new(10); assert_eq!(poly.len(), 10); } @@ -15,9 +15,9 @@ pub fn poly_pad_works_rand() { for _k in 0..256 { let poly_length: usize = (1 + (rng.next_u64() % 1000)) as usize; - let mut poly = FsPoly::new(poly_length); + let mut poly = CtPoly::new(poly_length); for i in 0..poly.len() { - poly.set_coeff_at(i, &FsFr::rand()); + poly.set_coeff_at(i, &CtFr::rand()); } let padded_poly = poly.pad(1000); @@ -32,43 +32,43 @@ pub fn poly_pad_works_rand() { pub fn poly_eval_check() { let n: usize = 10; - let mut poly = FsPoly::new(n); + let mut poly = CtPoly::new(n); for i in 0..n { - let fr = FsFr::from_u64((i + 1) as u64); + let fr = CtFr::from_u64((i + 1) as u64); poly.set_coeff_at(i, &fr); } - let expected = FsFr::from_u64((n * (n + 1) / 2) as u64); - let actual = poly.eval(&FsFr::one()); + let expected = CtFr::from_u64((n * (n + 1) / 2) as u64); + let actual = poly.eval(&CtFr::one()); assert!(expected.equals(&actual)); } pub fn poly_eval_0_check() { let n: usize = 7; let a: usize = 597; - let mut poly = FsPoly::new(n); + let mut poly = CtPoly::new(n); for i in 0..n { - let fr = FsFr::from_u64((i + a) as u64); + let fr = CtFr::from_u64((i + a) as u64); poly.set_coeff_at(i, &fr); } - let expected = FsFr::from_u64(a as u64); - let actual = poly.eval(&FsFr::zero()); + let expected = CtFr::from_u64(a as u64); + let actual = poly.eval(&CtFr::zero()); assert!(expected.equals(&actual)); } pub fn poly_eval_nil_check() { let n: usize = 0; - let poly = FsPoly::new(n); - let actual = poly.eval(&FsFr::one()); - assert!(actual.equals(&FsFr::zero())); + let poly = CtPoly::new(n); + let actual = poly.eval(&CtFr::one()); + assert!(actual.equals(&CtFr::zero())); } pub fn poly_inverse_simple_0() { // 1 / (1 - x) = 1 + x + x^2 + ... let d: usize = 16; - let mut p = FsPoly::new(2); - p.set_coeff_at(0, &FsFr::one()); - p.set_coeff_at(1, &FsFr::one()); - p.set_coeff_at(1, &FsFr::negate(&p.get_coeff_at(1))); + let mut p = CtPoly::new(2); + p.set_coeff_at(0, &CtFr::one()); + p.set_coeff_at(1, &CtFr::one()); + p.set_coeff_at(1, &CtFr::negate(&p.get_coeff_at(1))); let result = p.inverse(d); assert!(result.is_ok()); let q = result.unwrap(); @@ -80,16 +80,16 @@ pub fn poly_inverse_simple_0() { pub fn poly_inverse_simple_1() { // 1 / (1 + x) = 1 - x + x^2 - ... let d: usize = 16; - let mut p = FsPoly::new(2); - p.set_coeff_at(0, &FsFr::one()); - p.set_coeff_at(1, &FsFr::one()); + let mut p = CtPoly::new(2); + p.set_coeff_at(0, &CtFr::one()); + p.set_coeff_at(1, &CtFr::one()); let result = p.inverse(d); assert!(result.is_ok()); let q = result.unwrap(); for i in 0..d { let mut tmp = q.get_coeff_at(i); if i & 1 != 0 { - tmp = FsFr::negate(&tmp); + tmp = CtFr::negate(&tmp); } assert!(tmp.is_one()); } @@ -173,15 +173,15 @@ fn test_data(a: usize, b: usize) -> Vec { test_data[a][b].clone() } -fn new_test_poly(coeffs: &[i64]) -> FsPoly { - let mut p = FsPoly::new(0); +fn new_test_poly(coeffs: &[i64]) -> CtPoly { + let mut p = CtPoly::new(0); for &coeff in coeffs.iter() { if coeff >= 0 { - let c = FsFr::from_u64(coeff as u64); + let c = CtFr::from_u64(coeff as u64); p.coeffs.push(c); } else { - let c = FsFr::from_u64((-coeff) as u64); + let c = CtFr::from_u64((-coeff) as u64); let negc = c.negate(); p.coeffs.push(negc); } @@ -200,9 +200,9 @@ pub fn poly_div_long_test() { let divided_data = test_data(i, 0); let divisor_data = test_data(i, 1); let expected_data = test_data(i, 2); - let mut dividend: FsPoly = new_test_poly(÷d_data); - let divisor: FsPoly = new_test_poly(&divisor_data); - let expected: FsPoly = new_test_poly(&expected_data); + let mut dividend: CtPoly = new_test_poly(÷d_data); + let divisor: CtPoly = new_test_poly(&divisor_data); + let expected: CtPoly = new_test_poly(&expected_data); let actual = dividend.long_div(&divisor).unwrap(); @@ -223,9 +223,9 @@ pub fn poly_div_fast_test() { let divided_data = test_data(i, 0); let divisor_data = test_data(i, 1); let expected_data = test_data(i, 2); - let mut dividend: FsPoly = new_test_poly(÷d_data); - let divisor: FsPoly = new_test_poly(&divisor_data); - let expected: FsPoly = new_test_poly(&expected_data); + let mut dividend: CtPoly = new_test_poly(÷d_data); + let divisor: CtPoly = new_test_poly(&divisor_data); + let expected: CtPoly = new_test_poly(&expected_data); let actual = dividend.fast_div(&divisor).unwrap(); @@ -237,12 +237,12 @@ pub fn poly_div_fast_test() { } pub fn test_poly_div_by_zero() { - let mut dividend = FsPoly::new(2); + let mut dividend = CtPoly::new(2); - dividend.set_coeff_at(0, &FsFr::from_u64(1)); - dividend.set_coeff_at(1, &FsFr::from_u64(1)); + dividend.set_coeff_at(0, &CtFr::from_u64(1)); + dividend.set_coeff_at(1, &CtFr::from_u64(1)); - let divisor = FsPoly::new(0); + let divisor = CtPoly::new(0); let dummy = dividend.div(&divisor); assert!(dummy.is_err()); @@ -254,9 +254,9 @@ pub fn poly_mul_direct_test() { let coeffs2 = test_data(i, 1); let coeffs3 = test_data(i, 0); - let mut multiplicand: FsPoly = new_test_poly(&coeffs1); - let mut multiplier: FsPoly = new_test_poly(&coeffs2); - let expected: FsPoly = new_test_poly(&coeffs3); + let mut multiplicand: CtPoly = new_test_poly(&coeffs1); + let mut multiplier: CtPoly = new_test_poly(&coeffs2); + let expected: CtPoly = new_test_poly(&coeffs3); let result0 = multiplicand.mul_direct(&multiplier, coeffs3.len()).unwrap(); for j in 0..result0.len() { @@ -282,9 +282,9 @@ pub fn poly_mul_fft_test() { let coeffs2 = test_data(i, 1); let coeffs3 = test_data(i, 0); - let multiplicand: FsPoly = new_test_poly(&coeffs1); - let multiplier: FsPoly = new_test_poly(&coeffs2); - let expected: FsPoly = new_test_poly(&coeffs3); + let multiplicand: CtPoly = new_test_poly(&coeffs1); + let multiplier: CtPoly = new_test_poly(&coeffs2); + let expected: CtPoly = new_test_poly(&coeffs3); let result0 = multiplicand.mul_fft(&multiplier, coeffs3.len()).unwrap(); for j in 0..result0.len() { @@ -304,15 +304,15 @@ pub fn poly_mul_random() { for _k in 0..256 { let multiplicand_length: usize = (1 + (rng.next_u64() % 1000)) as usize; - let mut multiplicand = FsPoly::new(multiplicand_length); + let mut multiplicand = CtPoly::new(multiplicand_length); for i in 0..multiplicand.len() { - multiplicand.set_coeff_at(i, &FsFr::rand()); + multiplicand.set_coeff_at(i, &CtFr::rand()); } let multiplier_length: usize = (1 + (rng.next_u64() % 1000)) as usize; - let mut multiplier = FsPoly::new(multiplier_length); + let mut multiplier = CtPoly::new(multiplier_length); for i in 0..multiplier.len() { - multiplier.set_coeff_at(i, &FsFr::rand()); + multiplier.set_coeff_at(i, &CtFr::rand()); } if multiplicand.get_coeff_at(multiplicand.len() - 1).is_zero() { @@ -340,15 +340,15 @@ pub fn poly_div_random() { let dividend_length: usize = (2 + (rng.next_u64() % 1000)) as usize; let divisor_length: usize = 1 + ((rng.next_u64() as usize) % dividend_length); - let mut dividend = FsPoly::new(dividend_length); - let mut divisor = FsPoly::new(divisor_length); + let mut dividend = CtPoly::new(dividend_length); + let mut divisor = CtPoly::new(divisor_length); for i in 0..dividend_length { - dividend.set_coeff_at(i, &FsFr::rand()); + dividend.set_coeff_at(i, &CtFr::rand()); } for i in 0..divisor_length { - divisor.set_coeff_at(i, &FsFr::rand()); + divisor.set_coeff_at(i, &CtFr::rand()); } //Ensure that the polynomials' orders corresponds to their lengths diff --git a/constantine/tests/poly.rs b/constantine/tests/poly.rs index 9c9de312e..87e9c1392 100644 --- a/constantine/tests/poly.rs +++ b/constantine/tests/poly.rs @@ -9,9 +9,9 @@ mod tests { poly_inverse_simple_0, poly_inverse_simple_1, poly_mul_direct_test, poly_mul_fft_test, poly_mul_random, poly_test_div, }; - use rust_kzg_blst::types::fft_settings::FsFFTSettings; - use rust_kzg_blst::types::fr::FsFr; - use rust_kzg_blst::types::poly::FsPoly; + use rust_kzg_constantine::types::fft_settings::CtFFTSettings; + use rust_kzg_constantine::types::fr::CtFr; + use rust_kzg_constantine::types::poly::CtPoly; // Local tests // #[test] @@ -85,71 +85,71 @@ mod tests { // Shared tests #[test] fn create_poly_of_length_ten_() { - create_poly_of_length_ten::() + create_poly_of_length_ten::() } #[test] fn poly_eval_check_() { - poly_eval_check::() + poly_eval_check::() } #[test] fn poly_eval_0_check_() { - poly_eval_0_check::() + poly_eval_0_check::() } #[test] fn poly_eval_nil_check_() { - poly_eval_nil_check::() + poly_eval_nil_check::() } #[test] fn poly_inverse_simple_0_() { - poly_inverse_simple_0::() + poly_inverse_simple_0::() } #[test] fn poly_inverse_simple_1_() { - poly_inverse_simple_1::() + poly_inverse_simple_1::() } #[test] fn poly_test_div_() { - poly_test_div::() + poly_test_div::() } #[test] fn poly_div_by_zero_() { - poly_div_by_zero::() + poly_div_by_zero::() } #[test] fn poly_mul_direct_test_() { - poly_mul_direct_test::() + poly_mul_direct_test::() } #[test] fn poly_mul_fft_test_() { - poly_mul_fft_test::() + poly_mul_fft_test::() } #[test] fn poly_mul_random_() { - poly_mul_random::() + poly_mul_random::() } #[test] fn poly_div_random_() { - poly_div_random::() + poly_div_random::() } #[test] fn poly_div_long_test_() { - poly_div_long_test::() + poly_div_long_test::() } #[test] fn poly_div_fast_test_() { - poly_div_fast_test::() + poly_div_fast_test::() } } diff --git a/constantine/tests/recovery.rs b/constantine/tests/recovery.rs index c36668a14..59ccff18b 100644 --- a/constantine/tests/recovery.rs +++ b/constantine/tests/recovery.rs @@ -7,23 +7,23 @@ mod tests { // uncomment to use the local tests //use crate::local_recovery::{recover_random, recover_simple}; - use rust_kzg_blst::types::fft_settings::FsFFTSettings; - use rust_kzg_blst::types::fr::FsFr; - use rust_kzg_blst::types::poly::FsPoly; + use rust_kzg_constantine::types::fft_settings::CtFFTSettings; + use rust_kzg_constantine::types::fr::CtFr; + use rust_kzg_constantine::types::poly::CtPoly; // Shared tests #[test] fn recover_simple_() { - recover_simple::(); + recover_simple::(); } #[test] fn recover_random_() { - recover_random::(); + recover_random::(); } #[test] fn more_than_half_missing_() { - more_than_half_missing::(); + more_than_half_missing::(); } } diff --git a/constantine/tests/zero_poly.rs b/constantine/tests/zero_poly.rs index 67cded13a..8da771e92 100644 --- a/constantine/tests/zero_poly.rs +++ b/constantine/tests/zero_poly.rs @@ -4,42 +4,42 @@ mod tests { check_test_data, reduce_partials_random, test_reduce_partials, zero_poly_252, zero_poly_all_but_one, zero_poly_known, zero_poly_random, }; - use rust_kzg_blst::types::fft_settings::FsFFTSettings; - use rust_kzg_blst::types::fr::FsFr; - use rust_kzg_blst::types::poly::FsPoly; + use rust_kzg_constantine::types::fft_settings::CtFFTSettings; + use rust_kzg_constantine::types::fr::CtFr; + use rust_kzg_constantine::types::poly::CtPoly; #[test] fn test_reduce_partials_() { - test_reduce_partials::(); + test_reduce_partials::(); } #[test] fn reduce_partials_random_() { - reduce_partials_random::(); + reduce_partials_random::(); } #[test] fn check_test_data_() { - check_test_data::(); + check_test_data::(); } #[test] fn zero_poly_known_() { - zero_poly_known::(); + zero_poly_known::(); } #[test] fn zero_poly_random_() { - zero_poly_random::(); + zero_poly_random::(); } #[test] fn zero_poly_all_but_one_() { - zero_poly_all_but_one::(); + zero_poly_all_but_one::(); } #[test] fn zero_poly_252_() { - zero_poly_252::(); + zero_poly_252::(); } } diff --git a/kzg-bench/src/lib.rs b/kzg-bench/src/lib.rs index 6584df715..f8be54e99 100644 --- a/kzg-bench/src/lib.rs +++ b/kzg-bench/src/lib.rs @@ -1,3 +1,11 @@ +use std::{env::set_current_dir, path::Path}; + +use kzg::eip_4844::TRUSTED_SETUP_PATH; + pub mod benches; pub mod test_vectors; pub mod tests; + +pub fn set_trusted_setup_dir() { + set_current_dir(env!("CARGO_MANIFEST_DIR")).unwrap(); +} \ No newline at end of file From 6f0ae50840139cc7e916f8872fa7b5d6020c827d Mon Sep 17 00:00:00 2001 From: Domas Kalinauskas Date: Sun, 31 Dec 2023 13:32:46 +0200 Subject: [PATCH 04/17] Add mixed kzg settings, benchmarks/tests for constantine impls of eip4844 funcs --- constantine/benches/eip_4844_constantine.rs | 42 +++ .../benches/eip_4844_constantine_no_conv.rs | 173 +++++++++ constantine/src/lib.rs | 1 + .../src/mixed_kzg_settings/mixed_eip_4844.rs | 253 +++++++++++++ .../mixed_kzg_settings/mixed_kzg_settings.rs | 235 ++++++++++++ constantine/src/mixed_kzg_settings/mod.rs | 2 + constantine/tests/eip_4844_constantine.rs | 350 ++++++++++++++++++ 7 files changed, 1056 insertions(+) create mode 100644 constantine/benches/eip_4844_constantine.rs create mode 100644 constantine/benches/eip_4844_constantine_no_conv.rs create mode 100644 constantine/src/mixed_kzg_settings/mixed_eip_4844.rs create mode 100644 constantine/src/mixed_kzg_settings/mixed_kzg_settings.rs create mode 100644 constantine/src/mixed_kzg_settings/mod.rs create mode 100644 constantine/tests/eip_4844_constantine.rs diff --git a/constantine/benches/eip_4844_constantine.rs b/constantine/benches/eip_4844_constantine.rs new file mode 100644 index 000000000..45d3e306e --- /dev/null +++ b/constantine/benches/eip_4844_constantine.rs @@ -0,0 +1,42 @@ +// Same as eip_4844.rs, but using constantine implementations of verification functions + +use criterion::{criterion_group, criterion_main, Criterion}; +use kzg::eip_4844::{ + blob_to_kzg_commitment_rust, bytes_to_blob, compute_blob_kzg_proof_rust, + compute_kzg_proof_rust, verify_blob_kzg_proof_batch_rust, verify_blob_kzg_proof_rust, + verify_kzg_proof_rust, +}; +use kzg_bench::benches::eip_4844::bench_eip_4844; +use rust_kzg_constantine::{ + eip_4844::load_trusted_setup_filename_rust, + mixed_kzg_settings::mixed_eip_4844::load_trusted_setup_filename_mixed, + mixed_kzg_settings::{ + mixed_eip_4844::{ + blob_to_kzg_commitment_mixed, compute_blob_kzg_proof_mixed, compute_kzg_proof_mixed, + verify_blob_kzg_proof_batch_mixed, verify_blob_kzg_proof_mixed, verify_kzg_proof_mixed, + }, + mixed_kzg_settings::MixedKzgSettings, + }, + types::{ + fft_settings::CtFFTSettings, fr::CtFr, g1::CtG1, g2::CtG2, kzg_settings::CtKZGSettings, + poly::CtPoly, + }, +}; + +fn bench_eip_4844_constantine_(c: &mut Criterion) { + // Mixed KZG eip_4844 test - lots of conversions so not indicative of 'true' performance + bench_eip_4844::( + c, + &load_trusted_setup_filename_mixed, + &blob_to_kzg_commitment_mixed, + &bytes_to_blob, + &compute_kzg_proof_mixed, + &verify_kzg_proof_mixed, + &compute_blob_kzg_proof_mixed, + &verify_blob_kzg_proof_mixed, + &verify_blob_kzg_proof_batch_mixed, + ); +} + +criterion_group!(benches, bench_eip_4844_constantine_); +criterion_main!(benches); diff --git a/constantine/benches/eip_4844_constantine_no_conv.rs b/constantine/benches/eip_4844_constantine_no_conv.rs new file mode 100644 index 000000000..64d3bb427 --- /dev/null +++ b/constantine/benches/eip_4844_constantine_no_conv.rs @@ -0,0 +1,173 @@ +// Same as eip_4844.rs, but using constantine implementations of verification functions + +use std::env::set_current_dir; +use std::path::Path; + +use kzg_bench::set_trusted_setup_dir; +use kzg_bench::tests::eip_4844::{generate_random_blob_bytes, generate_random_field_element_bytes}; +use constantine_ethereum_kzg::EthKzgContext; +use criterion::{BatchSize, BenchmarkId, Criterion, Throughput}; +use kzg::eip_4844::{TRUSTED_SETUP_PATH, BYTES_PER_BLOB}; +use kzg::{FFTSettings, Fr, KZGSettings, Poly, G1, G2}; +use criterion::{criterion_group, criterion_main}; +use kzg::eip_4844::{ + blob_to_kzg_commitment_rust, bytes_to_blob, compute_blob_kzg_proof_rust, + compute_kzg_proof_rust, verify_blob_kzg_proof_batch_rust, verify_blob_kzg_proof_rust, + verify_kzg_proof_rust, +}; +use kzg_bench::benches::eip_4844::bench_eip_4844; +use rust_kzg_constantine::{ + eip_4844::load_trusted_setup_filename_rust, + mixed_kzg_settings::mixed_eip_4844::load_trusted_setup_filename_mixed, + mixed_kzg_settings::{ + mixed_eip_4844::{ + blob_to_kzg_commitment_mixed, compute_blob_kzg_proof_mixed, compute_kzg_proof_mixed, + verify_blob_kzg_proof_batch_mixed, verify_blob_kzg_proof_mixed, verify_kzg_proof_mixed, + }, + mixed_kzg_settings::{MixedKzgSettings, CttContext}, + }, + types::{ + fft_settings::CtFFTSettings, fr::CtFr, g1::CtG1, g2::CtG2, kzg_settings::CtKZGSettings, + poly::CtPoly, + }, +}; + +fn bench_eip_4844_constantine_no_conv_(c: &mut Criterion) { + set_trusted_setup_dir(); + let ctx = CttContext::new(Path::new(TRUSTED_SETUP_PATH)).unwrap(); + let mut rng = rand::thread_rng(); + const MAX_COUNT: usize = 64; + + let blobs: Vec<[u8; BYTES_PER_BLOB]> = (0..MAX_COUNT) + .map(|_| { + generate_random_blob_bytes(&mut rng) + }) + .collect(); + + let commitments: Vec<[u8; 48]> = blobs + .iter() + .map(|blob| ctx.ctx.blob_to_kzg_commitment(blob).unwrap()) + .collect(); + + let proofs: Vec<[u8; 48]> = blobs + .iter() + .zip(commitments.iter()) + .map(|(blob, commitment)| ctx.ctx.compute_blob_kzg_proof(blob, commitment).unwrap()) + .collect(); + + let fields: Vec<[u8; 32]> = (0..MAX_COUNT) + .map(|_| { + generate_random_field_element_bytes(&mut rng) + }) + .collect(); + + c.bench_function("blob_to_kzg_commitment", |b| { + #[cfg(feature = "parallel")] + b.iter(|| ctx.ctx.blob_to_kzg_commitment_parallel(&ctx.pool, blobs.first().unwrap())); + + #[cfg(not(feature = "parallel"))] + b.iter(|| ctx.ctx.blob_to_kzg_commitment(blobs.first().unwrap())); + }); + + c.bench_function("compute_kzg_proof", |b| { + #[cfg(feature = "parallel")] + b.iter(|| ctx.ctx.compute_kzg_proof_parallel(&ctx.pool, blobs.first().unwrap(), fields.first().unwrap())); + + #[cfg(not(feature = "parallel"))] + b.iter(|| ctx.ctx.compute_kzg_proof(blobs.first().unwrap(), fields.first().unwrap())); + }); + + c.bench_function("verify_kzg_proof", |b| { + b.iter(|| { + ctx.ctx.verify_kzg_proof( + commitments.first().unwrap(), + fields.first().unwrap(), + fields.first().unwrap(), + proofs.first().unwrap(), + ) + .unwrap() + }) + }); + + c.bench_function("compute_blob_kzg_proof", |b| { + #[cfg(feature = "parallel")] + b.iter(|| ctx.ctx.compute_blob_kzg_proof_parallel(&ctx.pool, blobs.first().unwrap(), commitments.first().unwrap())); + + #[cfg(not(feature = "parallel"))] + b.iter(|| ctx.ctx.compute_blob_kzg_proof(blobs.first().unwrap(), commitments.first().unwrap())); + }); + + c.bench_function("verify_blob_kzg_proof", |b| { + #[cfg(feature = "parallel")] + b.iter(|| { + ctx.ctx.verify_blob_kzg_proof_parallel( + &ctx.pool, + blobs.first().unwrap(), + commitments.first().unwrap(), + proofs.first().unwrap(), + ) + .unwrap() + }); + + #[cfg(not(feature = "parallel"))] + b.iter(|| { + ctx.ctx.verify_blob_kzg_proof( + blobs.first().unwrap(), + commitments.first().unwrap(), + proofs.first().unwrap(), + ) + .unwrap() + }); + }); + + let mut group = c.benchmark_group("verify_blob_kzg_proof_batch"); + let rand_thing = [0u8; 32]; + for count in [1, 2, 4, 8, 16, 32, 64] { + group.throughput(Throughput::Elements(count as u64)); + group.bench_with_input(BenchmarkId::from_parameter(count), &count, |b, &count| { + b.iter_batched_ref( + || { + let blobs_subset = blobs + .clone() + .into_iter() + .take(count) + .collect::>(); + let commitments_subset = commitments + .clone() + .into_iter() + .take(count) + .collect::>(); + let proofs_subset = + proofs.clone().into_iter().take(count).collect::>(); + + (blobs_subset, commitments_subset, proofs_subset) + }, + |(blobs_subset, commitments_subset, proofs_subset)| { + #[cfg(feature = "parallel")] + ctx.ctx.verify_blob_kzg_proof_batch_parallel( + &ctx.pool, + blobs_subset, + commitments_subset, + proofs_subset, + &rand_thing, + ) + .unwrap(); + + #[cfg(not(feature = "parallel"))] + ctx.ctx.verify_blob_kzg_proof_batch( + blobs_subset, + commitments_subset, + proofs_subset, + &rand_thing, + ) + .unwrap(); + }, + BatchSize::LargeInput, + ); + }); + } + group.finish(); +} + +criterion_group!(benches, bench_eip_4844_constantine_no_conv_); +criterion_main!(benches); diff --git a/constantine/src/lib.rs b/constantine/src/lib.rs index b228addf7..9bc9424ba 100644 --- a/constantine/src/lib.rs +++ b/constantine/src/lib.rs @@ -7,6 +7,7 @@ pub mod fft_fr; pub mod fft_g1; pub mod fk20_proofs; pub mod kzg_proofs; +pub mod mixed_kzg_settings; pub mod recovery; pub mod types; pub mod utils; diff --git a/constantine/src/mixed_kzg_settings/mixed_eip_4844.rs b/constantine/src/mixed_kzg_settings/mixed_eip_4844.rs new file mode 100644 index 000000000..1f1a0c3d4 --- /dev/null +++ b/constantine/src/mixed_kzg_settings/mixed_eip_4844.rs @@ -0,0 +1,253 @@ +use std::path::Path; + +// use crate:: +use crate::mixed_kzg_settings::mixed_kzg_settings::MixedKzgSettings; + +use crate::types::g1::CtG1Affine; +use crate::types::{fft_settings::CtFFTSettings, fr::CtFr, g1::CtG1, g2::CtG2, poly::CtPoly}; + +use kzg::eip_4844::{ + blob_to_kzg_commitment_rust, compute_blob_kzg_proof_rust, compute_kzg_proof_rust, + verify_blob_kzg_proof_batch_rust, verify_blob_kzg_proof_rust, verify_kzg_proof_rust, + BYTES_PER_BLOB, FIELD_ELEMENTS_PER_BLOB, +}; +use kzg::{Fr, G1}; + +use super::mixed_kzg_settings::LocalToStr; + +fn blob_fr_to_byte_inplace(blob: &[CtFr], inplace: &mut [u8; BYTES_PER_BLOB]) -> Option { + if blob.len() != FIELD_ELEMENTS_PER_BLOB { + return Some("blob length is not equal to FIELD_ELEMENTS_PER_BLOB".to_string()); + } + + for i in 0..FIELD_ELEMENTS_PER_BLOB { + inplace[i * 32..(i + 1) * 32].copy_from_slice(&blob[i].to_bytes()); + } + + None +} + +fn blob_fr_to_byte(blob: &[CtFr]) -> Result<[u8; BYTES_PER_BLOB], String> { + if blob.len() != FIELD_ELEMENTS_PER_BLOB { + return Err("blob length is not equal to FIELD_ELEMENTS_PER_BLOB".to_string()); + } + + let mut blob_bytes = [0u8; BYTES_PER_BLOB]; + for i in 0..FIELD_ELEMENTS_PER_BLOB { + blob_bytes[i * 32..(i + 1) * 32].copy_from_slice(&blob[i].to_bytes()); + } + + Ok(blob_bytes) + // unsafe { Ok(std::mem::transmute(blob.as_ptr() as *const [u8; BYTES_PER_BLOB])) } +} + +pub fn load_trusted_setup_filename_mixed(filepath: &str) -> Result { + MixedKzgSettings::new_from_path(Path::new(filepath)) +} + +pub fn blob_to_kzg_commitment_mixed( + blob: &[CtFr], + settings: &MixedKzgSettings, +) -> Result { + match settings { + MixedKzgSettings::Constantine(ctt_context) => { + let blob_bytes = blob_fr_to_byte(blob)?; + + #[cfg(feature = "parallel")] + let res = ctt_context + .ctx + .blob_to_kzg_commitment_parallel(&ctt_context.pool, &blob_bytes); + + #[cfg(not(feature = "parallel"))] + let res = ctt_context.ctx.blob_to_kzg_commitment(&blob_bytes); + + match res { + Ok(commitment) => CtG1::from_bytes(&commitment), + Err(x) => Err(x.to_string()), + } + // return blob_to_kzg_commitment_rust(blob, ctt_context); + } + MixedKzgSettings::Generic(generic_context) => { + return blob_to_kzg_commitment_rust(blob, generic_context); + } + } +} + +pub fn compute_kzg_proof_mixed( + blob: &[CtFr], + z: &CtFr, + s: &MixedKzgSettings, +) -> Result<(CtG1, CtFr), String> { + match s { + MixedKzgSettings::Constantine(ctt_context) => { + let blob_bytes = blob_fr_to_byte(blob)?; + unsafe { + let res = ctt_context + .ctx + .compute_kzg_proof(&blob_bytes, &z.to_bytes()); + match res { + // FIXME: Might not need the from_bytes on Fr here + Ok((proof, y)) => Ok((CtG1::from_bytes(&proof)?, CtFr::from_bytes(&y)?)), + Err(x) => Err(x.to_string()), + } + } + } + MixedKzgSettings::Generic(generic_context) => { + return compute_kzg_proof_rust(blob, z, generic_context); + } + } +} + +pub fn compute_blob_kzg_proof_mixed( + blob: &[CtFr], + commitment: &CtG1, + ts: &MixedKzgSettings, +) -> Result { + match ts { + MixedKzgSettings::Constantine(ctt_context) => { + let blob_bytes = blob_fr_to_byte(blob)?; + + #[cfg(feature = "parallel")] + let res = ctt_context.ctx.compute_blob_kzg_proof_parallel( + &ctt_context.pool, + &blob_bytes, + &commitment.to_bytes(), + ); + + #[cfg(not(feature = "parallel"))] + let res = ctt_context + .ctx + .compute_blob_kzg_proof(&blob_bytes, &commitment.to_bytes()); + + match res { + Ok(proof) => CtG1::from_bytes(&proof), + Err(x) => Err(x.to_string()), + } + } + MixedKzgSettings::Generic(generic_context) => { + return compute_blob_kzg_proof_rust(blob, commitment, generic_context); + } + } +} + +pub fn verify_kzg_proof_mixed( + commitment: &CtG1, + z: &CtFr, + y: &CtFr, + proof: &CtG1, + s: &MixedKzgSettings, +) -> Result { + match s { + MixedKzgSettings::Constantine(ctt_context) => { + let res = unsafe { + ctt_context.ctx.verify_kzg_proof( + &commitment.to_bytes(), + &z.to_bytes(), + &y.to_bytes(), + &proof.to_bytes(), + ) + }; + match res { + Ok(x) => Ok(x), + Err(x) => Err(x.to_string()), + } + } + MixedKzgSettings::Generic(generic_context) => { + return verify_kzg_proof_rust(commitment, z, y, proof, generic_context); + } + } +} + +pub fn verify_blob_kzg_proof_mixed( + blob: &[CtFr], + commitment_g1: &CtG1, + proof_g1: &CtG1, + ts: &MixedKzgSettings, +) -> Result { + match ts { + MixedKzgSettings::Constantine(ctt_context) => { + let blob_bytes = blob_fr_to_byte(blob)?; + + #[cfg(feature = "parallel")] + let res = ctt_context.ctx.verify_blob_kzg_proof_parallel( + &ctt_context.pool, + &blob_bytes, + &commitment_g1.to_bytes(), + &proof_g1.to_bytes(), + ); + + #[cfg(not(feature = "parallel"))] + let res = ctt_context.ctx.verify_blob_kzg_proof( + &blob_bytes, + &commitment_g1.to_bytes(), + &proof_g1.to_bytes(), + ); + + match res { + Ok(x) => Ok(x), + Err(x) => Err(x.to_string()), + } + } + MixedKzgSettings::Generic(generic_context) => { + return verify_blob_kzg_proof_rust(blob, commitment_g1, proof_g1, generic_context); + } + } +} + +pub fn verify_blob_kzg_proof_batch_mixed( + blobs: &[Vec], + commitments_g1: &[CtG1], + proofs_g1: &[CtG1], + ts: &MixedKzgSettings, +) -> Result { + match ts { + MixedKzgSettings::Constantine(ctt_context) => { + let mut blobs_storage = vec![[0u8; BYTES_PER_BLOB]; blobs.len()]; + for (i, blob) in blobs.into_iter().enumerate() { + let res = blob_fr_to_byte_inplace(blob, &mut blobs_storage[i]); + if res.is_some() { + return Err(res.unwrap()); + } + } + // let blobs = blobs.iter().map(blob_fr_to_byte_vec).collect::, _>>()?; + + let commitments = commitments_g1 + .iter() + .map(|x| x.to_bytes()) + .collect::>(); + let proofs_g1 = proofs_g1.iter().map(|x| x.to_bytes()).collect::>(); + + let rand_thing = [0u8; 32]; + + #[cfg(feature = "parallel")] + let res = ctt_context.ctx.verify_blob_kzg_proof_batch_parallel( + &ctt_context.pool, + blobs_storage.as_slice(), + commitments.as_slice(), + proofs_g1.as_slice(), + &rand_thing, + ); + + #[cfg(not(feature = "parallel"))] + let res = ctt_context.ctx.verify_blob_kzg_proof_batch( + blobs_storage.as_slice(), + commitments.as_slice(), + proofs_g1.as_slice(), + &rand_thing, + ); + + match res { + Ok(x) => Ok(x), + Err(x) => Err(x.to_string()), + } + } + MixedKzgSettings::Generic(generic_context) => { + return verify_blob_kzg_proof_batch_rust( + blobs, + commitments_g1, + proofs_g1, + generic_context, + ); + } + } +} diff --git a/constantine/src/mixed_kzg_settings/mixed_kzg_settings.rs b/constantine/src/mixed_kzg_settings/mixed_kzg_settings.rs new file mode 100644 index 000000000..1c8c872e3 --- /dev/null +++ b/constantine/src/mixed_kzg_settings/mixed_kzg_settings.rs @@ -0,0 +1,235 @@ +use std::path::Path; + +use crate::types::{ + fft_settings::CtFFTSettings, fr::CtFr, g1::CtG1, g2::CtG2, + kzg_settings::CtKZGSettings as GenericContext, poly::CtPoly, +}; +use constantine_core::Threadpool as CttThreadpool; +use constantine_ethereum_kzg::EthKzgContext as CttEthKzgContext; +use constantine_sys::{ctt_eth_kzg_status, ctt_eth_trusted_setup_status}; +use kzg::KZGSettings; + +pub struct CttContext { + pub ctx: CttEthKzgContext, + pub pool: CttThreadpool, +} + +impl CttContext { + pub fn new(path: &Path) -> Result { + let res = CttEthKzgContext::load_trusted_setup(path); + match res { + Ok(constantine_context) => Ok(Self { + ctx: constantine_context, + pool: CttThreadpool::new(get_thr_count()), + }), + Err(x) => Err(x.to_string()), + } + } +} + +fn get_thr_count() -> usize { + #[cfg(feature = "parallel")] + return constantine_core::hardware::get_num_threads_os(); + + #[cfg(not(feature = "parallel"))] + return 1; +} + +// Constantine requires loading from path, but for UT not always possible +pub enum MixedKzgSettings { + Constantine(CttContext), + Generic(GenericContext), +} + +pub trait LocalToStr { + fn to_string(&self) -> String; +} + +impl LocalToStr for ctt_eth_trusted_setup_status { + fn to_string(&self) -> String { + match self { + ctt_eth_trusted_setup_status::cttEthTS_InvalidFile => "invalid file".to_owned(), + ctt_eth_trusted_setup_status::cttEthTS_MissingOrInaccessibleFile => { + "missing or inaccessible file".to_owned() + } + ctt_eth_trusted_setup_status::cttEthTS_Success => "success".to_owned(), + } + } +} + +impl LocalToStr for ctt_eth_kzg_status { + fn to_string(&self) -> String { + format!("{:?}", self) + } +} + +impl MixedKzgSettings { + pub fn new( + secret_g1: &[CtG1], + secret_g2: &[CtG2], + length: usize, + fft_settings: &CtFFTSettings, + ) -> Result { + let res = GenericContext::new(secret_g1, secret_g2, length, fft_settings); + match res { + Ok(generic_context) => Ok(Self::Generic(generic_context)), + Err(x) => Err(x), + } + } + + pub fn new_from_path(path: &Path) -> Result { + let res = CttEthKzgContext::load_trusted_setup(path); + match res { + // #[cfg(feature = "parallel")] + Ok(constantine_context) => Ok(Self::Constantine(CttContext { + ctx: constantine_context, + pool: CttThreadpool::new(get_thr_count()), + })), + // #[cfg(not(feature = "parallel"))] + // Ok(constantine_context) => Ok(Self::Constantine(CttContext { + // ctx: constantine_context, + // })), + Err(x) => Err(x.to_string()), + } + } +} + +impl Default for MixedKzgSettings { + fn default() -> Self { + Self::Generic(GenericContext::default()) + } +} + +impl Clone for MixedKzgSettings { + fn clone(&self) -> Self { + match self { + Self::Constantine(arg0) => panic!("Cannot clone constantine context"), + Self::Generic(arg0) => Self::Generic(arg0.clone()), + } + } +} + +// Allow using MixedKzgSettings as KZGSettings stand-in +impl KZGSettings for MixedKzgSettings { + fn new( + secret_g1: &[CtG1], + secret_g2: &[CtG2], + length: usize, + fs: &CtFFTSettings, + ) -> Result { + MixedKzgSettings::new(secret_g1, secret_g2, length, fs) + } + + fn commit_to_poly(&self, p: &CtPoly) -> Result { + match self { + MixedKzgSettings::Constantine(constantine_context) => { + Err("Context not in generic format".to_string()) + } + MixedKzgSettings::Generic(generic_context) => generic_context.commit_to_poly(p), + } + } + + fn compute_proof_single(&self, p: &CtPoly, x: &CtFr) -> Result { + match self { + MixedKzgSettings::Constantine(constantine_context) => { + Err("Context not in generic format".to_string()) + } + MixedKzgSettings::Generic(generic_context) => { + generic_context.compute_proof_single(p, x) + } + } + } + + fn check_proof_single( + &self, + com: &CtG1, + proof: &CtG1, + x: &CtFr, + value: &CtFr, + ) -> Result { + match self { + MixedKzgSettings::Constantine(constantine_context) => { + Err("Context not in generic format".to_string()) + } + MixedKzgSettings::Generic(generic_context) => { + generic_context.check_proof_single(com, proof, x, value) + } + } + } + + fn compute_proof_multi(&self, p: &CtPoly, x: &CtFr, n: usize) -> Result { + match self { + MixedKzgSettings::Constantine(constantine_context) => { + Err("Context not in generic format".to_string()) + } + MixedKzgSettings::Generic(generic_context) => { + generic_context.compute_proof_multi(p, x, n) + } + } + } + + fn check_proof_multi( + &self, + com: &CtG1, + proof: &CtG1, + x: &CtFr, + values: &[CtFr], + n: usize, + ) -> Result { + match self { + MixedKzgSettings::Constantine(constantine_context) => { + Err("Context not in generic format".to_string()) + } + MixedKzgSettings::Generic(generic_context) => { + generic_context.check_proof_multi(com, proof, x, values, n) + } + } + } + + fn get_expanded_roots_of_unity_at(&self, i: usize) -> CtFr { + match self { + MixedKzgSettings::Constantine(constantine_context) => { + panic!("Context not in generic format") + } + MixedKzgSettings::Generic(generic_context) => { + generic_context.get_expanded_roots_of_unity_at(i) + } + } + } + + fn get_roots_of_unity_at(&self, i: usize) -> CtFr { + match self { + MixedKzgSettings::Constantine(constantine_context) => { + panic!("Context not in generic format") + } + MixedKzgSettings::Generic(generic_context) => generic_context.get_roots_of_unity_at(i), + } + } + + fn get_fft_settings(&self) -> &CtFFTSettings { + match self { + MixedKzgSettings::Constantine(constantine_context) => { + panic!("Context not in generic format") + } + MixedKzgSettings::Generic(generic_context) => generic_context.get_fft_settings(), + } + } + + fn get_g1_secret(&self) -> &[CtG1] { + match self { + MixedKzgSettings::Constantine(constantine_context) => { + panic!("Context not in generic format") + } + MixedKzgSettings::Generic(generic_context) => generic_context.get_g1_secret(), + } + } + + fn get_g2_secret(&self) -> &[CtG2] { + match self { + MixedKzgSettings::Constantine(constantine_context) => { + panic!("Context not in generic format") + } + MixedKzgSettings::Generic(generic_context) => generic_context.get_g2_secret(), + } + } +} diff --git a/constantine/src/mixed_kzg_settings/mod.rs b/constantine/src/mixed_kzg_settings/mod.rs new file mode 100644 index 000000000..9b6c1154f --- /dev/null +++ b/constantine/src/mixed_kzg_settings/mod.rs @@ -0,0 +1,2 @@ +pub mod mixed_eip_4844; +pub mod mixed_kzg_settings; diff --git a/constantine/tests/eip_4844_constantine.rs b/constantine/tests/eip_4844_constantine.rs new file mode 100644 index 000000000..92aa2b788 --- /dev/null +++ b/constantine/tests/eip_4844_constantine.rs @@ -0,0 +1,350 @@ +// EIP_4844 Tests, but using constantine binding functions +// Some tests are disabled because they rely on data that is not possible to pull from constantine + +#[cfg(test)] +mod tests { + use kzg::eip_4844::{ + blob_to_kzg_commitment_rust, blob_to_polynomial, bytes_to_blob, + compute_blob_kzg_proof_rust, compute_kzg_proof_rust, compute_powers, + evaluate_polynomial_in_evaluation_form, verify_blob_kzg_proof_batch_rust, + verify_blob_kzg_proof_rust, verify_kzg_proof_rust, + }; + use kzg::Fr; + + use kzg_bench::tests::eip_4844::{ + blob_to_kzg_commitment_test, bytes_to_bls_field_test, + compute_and_verify_blob_kzg_proof_fails_with_incorrect_proof_test, + compute_and_verify_blob_kzg_proof_test, + compute_and_verify_kzg_proof_fails_with_incorrect_proof_test, + compute_and_verify_kzg_proof_round_trip_test, + compute_and_verify_kzg_proof_within_domain_test, compute_kzg_proof_empty_blob_vector_test, + compute_kzg_proof_incorrect_blob_length_test, + compute_kzg_proof_incorrect_commitments_len_test, + compute_kzg_proof_incorrect_poly_length_test, compute_kzg_proof_incorrect_proofs_len_test, + compute_kzg_proof_test, compute_powers_test, test_vectors_blob_to_kzg_commitment, + test_vectors_compute_blob_kzg_proof, test_vectors_compute_kzg_proof, + test_vectors_verify_blob_kzg_proof, test_vectors_verify_blob_kzg_proof_batch, + test_vectors_verify_kzg_proof, validate_batched_input_test, + verify_kzg_proof_batch_fails_with_incorrect_proof_test, verify_kzg_proof_batch_test, + }; + use rust_kzg_constantine::consts::SCALE2_ROOT_OF_UNITY; + use rust_kzg_constantine::eip_4844::load_trusted_setup_filename_rust; + use rust_kzg_constantine::mixed_kzg_settings::mixed_eip_4844::{ + blob_to_kzg_commitment_mixed, compute_blob_kzg_proof_mixed, compute_kzg_proof_mixed, + load_trusted_setup_filename_mixed, verify_blob_kzg_proof_batch_mixed, + verify_blob_kzg_proof_mixed, verify_kzg_proof_mixed, + }; + use rust_kzg_constantine::mixed_kzg_settings::mixed_kzg_settings::MixedKzgSettings; + use rust_kzg_constantine::types::fft_settings::expand_root_of_unity; + use rust_kzg_constantine::types::{ + fft_settings::CtFFTSettings, fr::CtFr, g1::CtG1, g2::CtG2, kzg_settings::CtKZGSettings, + poly::CtPoly, + }; + + #[test] + pub fn bytes_to_bls_field_test_() { + bytes_to_bls_field_test::(); + } + + #[test] + pub fn compute_powers_test_() { + compute_powers_test::(&compute_powers); + } + + #[test] + pub fn blob_to_kzg_commitment_test_() { + blob_to_kzg_commitment_test::( + &load_trusted_setup_filename_mixed, + &blob_to_kzg_commitment_mixed, + ); + } + + // #[test] + // pub fn compute_kzg_proof_test_() { + // compute_kzg_proof_test::( + // &load_trusted_setup_filename_mixed, + // &compute_kzg_proof_mixed, + // &blob_to_polynomial, + // &evaluate_polynomial_in_evaluation_form, + // ); + // } + + // #[test] + // pub fn compute_and_verify_kzg_proof_round_trip_test_() { + // compute_and_verify_kzg_proof_round_trip_test::< + // CtFr, + // CtG1, + // CtG2, + // CtPoly, + // CtFFTSettings, + // MixedKzgSettings, + // >( + // &load_trusted_setup_filename_mixed, + // &blob_to_kzg_commitment_mixed, + // &bytes_to_blob, + // &compute_kzg_proof_mixed, + // &blob_to_polynomial, + // &evaluate_polynomial_in_evaluation_form, + // &verify_kzg_proof_mixed, + // ); + // } + + // #[test] + // pub fn compute_and_verify_kzg_proof_within_domain_test_() { + // compute_and_verify_kzg_proof_within_domain_test::< + // CtFr, + // CtG1, + // CtG2, + // CtPoly, + // CtFFTSettings, + // MixedKzgSettings, + // >( + // &load_trusted_setup_filename_mixed, + // &blob_to_kzg_commitment_mixed, + // &bytes_to_blob, + // &compute_kzg_proof_mixed, + // &blob_to_polynomial, + // &evaluate_polynomial_in_evaluation_form, + // &verify_kzg_proof_mixed, + // ); + // } + + // #[test] + // pub fn compute_and_verify_kzg_proof_fails_with_incorrect_proof_test_() { + // compute_and_verify_kzg_proof_fails_with_incorrect_proof_test::< + // CtFr, + // CtG1, + // CtG2, + // CtPoly, + // CtFFTSettings, + // MixedKzgSettings, + // >( + // &load_trusted_setup_filename_mixed, + // &blob_to_kzg_commitment_mixed, + // &bytes_to_blob, + // &compute_kzg_proof_mixed, + // &blob_to_polynomial, + // &evaluate_polynomial_in_evaluation_form, + // &verify_kzg_proof_mixed, + // ); + // } + + #[test] + pub fn compute_and_verify_blob_kzg_proof_test_() { + compute_and_verify_blob_kzg_proof_test::< + CtFr, + CtG1, + CtG2, + CtPoly, + CtFFTSettings, + MixedKzgSettings, + >( + &load_trusted_setup_filename_mixed, + &blob_to_kzg_commitment_mixed, + &bytes_to_blob, + &compute_blob_kzg_proof_mixed, + &verify_blob_kzg_proof_mixed, + ); + } + + #[test] + pub fn compute_and_verify_blob_kzg_proof_fails_with_incorrect_proof_test_() { + compute_and_verify_blob_kzg_proof_fails_with_incorrect_proof_test::< + CtFr, + CtG1, + CtG2, + CtPoly, + CtFFTSettings, + MixedKzgSettings, + >( + &load_trusted_setup_filename_mixed, + &blob_to_kzg_commitment_mixed, + &bytes_to_blob, + &compute_blob_kzg_proof_mixed, + &verify_blob_kzg_proof_mixed, + ); + } + + #[test] + pub fn verify_kzg_proof_batch_test_() { + verify_kzg_proof_batch_test::( + &load_trusted_setup_filename_mixed, + &blob_to_kzg_commitment_mixed, + &bytes_to_blob, + &compute_blob_kzg_proof_mixed, + &verify_blob_kzg_proof_batch_mixed, + ); + } + + #[test] + pub fn verify_kzg_proof_batch_fails_with_incorrect_proof_test_() { + verify_kzg_proof_batch_fails_with_incorrect_proof_test::< + CtFr, + CtG1, + CtG2, + CtPoly, + CtFFTSettings, + MixedKzgSettings, + >( + &load_trusted_setup_filename_mixed, + &blob_to_kzg_commitment_mixed, + &bytes_to_blob, + &compute_blob_kzg_proof_mixed, + &verify_blob_kzg_proof_batch_mixed, + ); + } + + #[test] + pub fn test_vectors_blob_to_kzg_commitment_() { + test_vectors_blob_to_kzg_commitment::< + CtFr, + CtG1, + CtG2, + CtPoly, + CtFFTSettings, + MixedKzgSettings, + >( + &load_trusted_setup_filename_mixed, + &blob_to_kzg_commitment_mixed, + &bytes_to_blob, + ); + } + + #[test] + pub fn test_vectors_compute_kzg_proof_() { + test_vectors_compute_kzg_proof::( + &load_trusted_setup_filename_mixed, + &compute_kzg_proof_mixed, + &bytes_to_blob, + ); + } + + #[test] + pub fn test_vectors_compute_blob_kzg_proof_() { + test_vectors_compute_blob_kzg_proof::< + CtFr, + CtG1, + CtG2, + CtPoly, + CtFFTSettings, + MixedKzgSettings, + >( + &load_trusted_setup_filename_mixed, + &bytes_to_blob, + &compute_blob_kzg_proof_mixed, + ); + } + + #[test] + pub fn test_vectors_verify_kzg_proof_() { + test_vectors_verify_kzg_proof::( + &load_trusted_setup_filename_mixed, + &verify_kzg_proof_mixed, + ); + } + + #[test] + pub fn test_vectors_verify_blob_kzg_proof_() { + test_vectors_verify_blob_kzg_proof::< + CtFr, + CtG1, + CtG2, + CtPoly, + CtFFTSettings, + MixedKzgSettings, + >( + &load_trusted_setup_filename_mixed, + &bytes_to_blob, + &verify_blob_kzg_proof_mixed, + ); + } + + #[test] + pub fn test_vectors_verify_blob_kzg_proof_batch_() { + test_vectors_verify_blob_kzg_proof_batch::< + CtFr, + CtG1, + CtG2, + CtPoly, + CtFFTSettings, + MixedKzgSettings, + >( + &load_trusted_setup_filename_mixed, + &bytes_to_blob, + &verify_blob_kzg_proof_batch_mixed, + ); + } + + #[test] + pub fn expand_root_of_unity_too_long() { + let out = expand_root_of_unity(&CtFr::from_u64_arr(&SCALE2_ROOT_OF_UNITY[1]), 1); + assert!(out.is_err()); + } + + #[test] + pub fn expand_root_of_unity_too_short() { + let out = expand_root_of_unity(&CtFr::from_u64_arr(&SCALE2_ROOT_OF_UNITY[1]), 3); + assert!(out.is_err()); + } + + #[test] + pub fn compute_kzg_proof_incorrect_blob_length() { + compute_kzg_proof_incorrect_blob_length_test::(&blob_to_polynomial); + } + + #[test] + pub fn compute_kzg_proof_incorrect_poly_length() { + compute_kzg_proof_incorrect_poly_length_test::< + CtPoly, + CtFr, + CtG1, + CtG2, + CtFFTSettings, + MixedKzgSettings, + >(&evaluate_polynomial_in_evaluation_form); + } + + #[test] + pub fn compute_kzg_proof_empty_blob_vector() { + compute_kzg_proof_empty_blob_vector_test::< + CtPoly, + CtFr, + CtG1, + CtG2, + CtFFTSettings, + MixedKzgSettings, + >(&verify_blob_kzg_proof_batch_mixed) + } + + #[test] + pub fn compute_kzg_proof_incorrect_commitments_len() { + compute_kzg_proof_incorrect_commitments_len_test::< + CtPoly, + CtFr, + CtG1, + CtG2, + CtFFTSettings, + MixedKzgSettings, + >(&verify_blob_kzg_proof_batch_mixed) + } + + #[test] + pub fn compute_kzg_proof_incorrect_proofs_len() { + compute_kzg_proof_incorrect_proofs_len_test::< + CtPoly, + CtFr, + CtG1, + CtG2, + CtFFTSettings, + MixedKzgSettings, + >(&verify_blob_kzg_proof_batch_mixed) + } + + #[test] + pub fn validate_batched_input() { + validate_batched_input_test::( + &verify_blob_kzg_proof_batch_mixed, + &load_trusted_setup_filename_mixed, + ) + } +} From e8d7b01108eca746e5faa23edb8cce9c6f82e5ba Mon Sep 17 00:00:00 2001 From: Domas Kalinauskas Date: Sun, 31 Dec 2023 10:21:55 +0200 Subject: [PATCH 05/17] bgmw + workflows --- .github/workflows/backend-benchmarks.yml | 4 +- .github/workflows/backend-tests.yml | 6 ++- .github/workflows/release.yml | 2 +- constantine/Cargo.toml | 3 +- constantine/benches/eip_4844.rs | 6 +-- constantine/benches/eip_4844_constantine.rs | 6 +-- constantine/benches/fk_20.rs | 7 +-- constantine/benches/kzg.rs | 7 +-- constantine/benches/lincomb.rs | 5 +- constantine/csharp.patch | 2 +- constantine/go.patch | 2 +- constantine/java.patch | 2 +- constantine/nodejs.patch | 2 +- constantine/python.patch | 2 +- constantine/rust.patch | 2 +- constantine/src/eip_4844.rs | 1 + constantine/src/kzg_proofs.rs | 7 ++- .../mixed_kzg_settings/mixed_kzg_settings.rs | 22 ++++---- constantine/src/types/fk20_multi_settings.rs | 5 +- constantine/src/types/fk20_single_settings.rs | 5 +- constantine/src/types/g1.rs | 11 +++- constantine/src/types/kzg_settings.rs | 14 ++++- constantine/tests/batch_adder.rs | 2 +- constantine/tests/bls12_381.rs | 7 +-- constantine/tests/eip_4844.rs | 43 +++++++-------- constantine/tests/eip_4844_constantine.rs | 52 ++++++++----------- constantine/tests/fk20_proofs.rs | 10 +++- constantine/tests/kzg_proofs.rs | 11 ++-- 28 files changed, 144 insertions(+), 104 deletions(-) diff --git a/.github/workflows/backend-benchmarks.yml b/.github/workflows/backend-benchmarks.yml index b6c27ed97..6a459343c 100644 --- a/.github/workflows/backend-benchmarks.yml +++ b/.github/workflows/backend-benchmarks.yml @@ -11,7 +11,7 @@ jobs: matrix: os: [ubuntu-latest, windows-latest, macos-11] # Excluding mcl for now due to they have different project layout - backend: [blst, zkcrypto, arkworks] + backend: [blst, zkcrypto, arkworks, constantine] include: # Setup exec_once_per_backend flag - os: ubuntu-latest @@ -23,6 +23,8 @@ jobs: support_ckzg: true - backend: arkworks support_ckzg: true + - backend: constantine + support_ckzg: true steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/backend-tests.yml b/.github/workflows/backend-tests.yml index cf865e4e1..4814e8744 100644 --- a/.github/workflows/backend-tests.yml +++ b/.github/workflows/backend-tests.yml @@ -11,7 +11,7 @@ jobs: matrix: os: [ubuntu-latest, windows-latest, macos-11] # Excluding mcl for now due to they have different project layout - backend: [blst, zkcrypto, arkworks] + backend: [blst, zkcrypto, arkworks, constantine] include: # Set default clippy flag to all-features - clippy-flag: --all-features @@ -36,6 +36,10 @@ jobs: - backend: arkworks support_wasm: true support_ckzg: true + - backend: constantine + # FIXME: Check for wasm support + support_wasm: false + support_ckzg: true steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index aa1635602..b10b14522 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - backend: [blst, zkcrypto, arkworks] + backend: [blst, zkcrypto, arkworks, constantine] target: [windows, linux] include: # Set target-name for target builds diff --git a/constantine/Cargo.toml b/constantine/Cargo.toml index 281eb210b..3d9057305 100644 --- a/constantine/Cargo.toml +++ b/constantine/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] -blst = { git = 'https://github.com/supranational/blst.git' } +blst = "0.3.11" kzg = { path = "../kzg", default-features = false } libc = { version = "0.2.148", default-features = false } once_cell = { version = "1.18.0", features = ["critical-section"], default-features = false } @@ -25,7 +25,6 @@ rand = "0.8.5" default = [ "std", "rand", - "constantine_msm" ] std = [ "hex/std", diff --git a/constantine/benches/eip_4844.rs b/constantine/benches/eip_4844.rs index 1979fa418..6ec241ded 100644 --- a/constantine/benches/eip_4844.rs +++ b/constantine/benches/eip_4844.rs @@ -8,13 +8,13 @@ use kzg_bench::benches::eip_4844::bench_eip_4844; use rust_kzg_constantine::{ eip_4844::load_trusted_setup_filename_rust, types::{ - fft_settings::CtFFTSettings, fr::CtFr, g1::CtG1, g2::CtG2, kzg_settings::CtKZGSettings, - poly::CtPoly, + fft_settings::CtFFTSettings, fr::CtFr, g1::{CtG1, CtG1Affine}, g2::CtG2, kzg_settings::CtKZGSettings, + poly::CtPoly, fp::CtFp, }, }; fn bench_eip_4844_(c: &mut Criterion) { - bench_eip_4844::( + bench_eip_4844::( c, &load_trusted_setup_filename_rust, &blob_to_kzg_commitment_rust, diff --git a/constantine/benches/eip_4844_constantine.rs b/constantine/benches/eip_4844_constantine.rs index 45d3e306e..2a6f0b4ba 100644 --- a/constantine/benches/eip_4844_constantine.rs +++ b/constantine/benches/eip_4844_constantine.rs @@ -18,14 +18,14 @@ use rust_kzg_constantine::{ mixed_kzg_settings::MixedKzgSettings, }, types::{ - fft_settings::CtFFTSettings, fr::CtFr, g1::CtG1, g2::CtG2, kzg_settings::CtKZGSettings, - poly::CtPoly, + fft_settings::CtFFTSettings, fr::CtFr, g1::{CtG1, CtG1Affine}, g2::CtG2, kzg_settings::CtKZGSettings, + poly::CtPoly, fp::CtFp, }, }; fn bench_eip_4844_constantine_(c: &mut Criterion) { // Mixed KZG eip_4844 test - lots of conversions so not indicative of 'true' performance - bench_eip_4844::( + bench_eip_4844::( c, &load_trusted_setup_filename_mixed, &blob_to_kzg_commitment_mixed, diff --git a/constantine/benches/fk_20.rs b/constantine/benches/fk_20.rs index df65bfcb1..f56a929bc 100644 --- a/constantine/benches/fk_20.rs +++ b/constantine/benches/fk_20.rs @@ -4,22 +4,23 @@ use kzg_bench::benches::fk20::{bench_fk_multi_da, bench_fk_single_da}; use rust_kzg_constantine::types::fft_settings::CtFFTSettings; use rust_kzg_constantine::types::fk20_multi_settings::CtFK20MultiSettings; use rust_kzg_constantine::types::fk20_single_settings::CtFK20SingleSettings; +use rust_kzg_constantine::types::fp::CtFp; use rust_kzg_constantine::types::fr::CtFr; -use rust_kzg_constantine::types::g1::CtG1; +use rust_kzg_constantine::types::g1::{CtG1, CtG1Affine}; use rust_kzg_constantine::types::g2::CtG2; use rust_kzg_constantine::types::kzg_settings::CtKZGSettings; use rust_kzg_constantine::types::poly::CtPoly; use rust_kzg_constantine::utils::generate_trusted_setup; fn bench_fk_single_da_(c: &mut Criterion) { - bench_fk_single_da::( + bench_fk_single_da::( c, &generate_trusted_setup, ) } fn bench_fk_multi_da_(c: &mut Criterion) { - bench_fk_multi_da::( + bench_fk_multi_da::( c, &generate_trusted_setup, ) diff --git a/constantine/benches/kzg.rs b/constantine/benches/kzg.rs index fd1273eb5..7985c8414 100644 --- a/constantine/benches/kzg.rs +++ b/constantine/benches/kzg.rs @@ -1,22 +1,23 @@ use criterion::{criterion_group, criterion_main, Criterion}; use kzg_bench::benches::kzg::{bench_commit_to_poly, bench_compute_proof_single}; use rust_kzg_constantine::types::fft_settings::CtFFTSettings; +use rust_kzg_constantine::types::fp::CtFp; use rust_kzg_constantine::types::fr::CtFr; -use rust_kzg_constantine::types::g1::CtG1; +use rust_kzg_constantine::types::g1::{CtG1, CtG1Affine}; use rust_kzg_constantine::types::g2::CtG2; use rust_kzg_constantine::types::kzg_settings::CtKZGSettings; use rust_kzg_constantine::types::poly::CtPoly; use rust_kzg_constantine::utils::generate_trusted_setup; fn bench_commit_to_poly_(c: &mut Criterion) { - bench_commit_to_poly::( + bench_commit_to_poly::( c, &generate_trusted_setup, ) } fn bench_compute_proof_single_(c: &mut Criterion) { - bench_compute_proof_single::( + bench_compute_proof_single::( c, &generate_trusted_setup, ) diff --git a/constantine/benches/lincomb.rs b/constantine/benches/lincomb.rs index ca9b1024d..e76c1e017 100644 --- a/constantine/benches/lincomb.rs +++ b/constantine/benches/lincomb.rs @@ -1,11 +1,12 @@ use criterion::{criterion_group, criterion_main, Criterion}; use kzg_bench::benches::lincomb::bench_g1_lincomb; use rust_kzg_constantine::kzg_proofs::g1_linear_combination; +use rust_kzg_constantine::types::fp::CtFp; use rust_kzg_constantine::types::fr::CtFr; -use rust_kzg_constantine::types::g1::CtG1; +use rust_kzg_constantine::types::g1::{CtG1, CtG1Affine}; fn bench_g1_lincomb_(c: &mut Criterion) { - bench_g1_lincomb::(c, &g1_linear_combination); + bench_g1_lincomb::(c, &g1_linear_combination); } criterion_group! { diff --git a/constantine/csharp.patch b/constantine/csharp.patch index 91d17ee27..9554ea165 100644 --- a/constantine/csharp.patch +++ b/constantine/csharp.patch @@ -16,7 +16,7 @@ index 5158aad..af3b2a8 100644 INCLUDE_DIRS = ../../src ../../blst/bindings -TARGETS = ckzg.c ../../src/c_kzg_4844.c ../../blst/$(BLST_OBJ) -+TARGETS = ckzg.c ../../../../target/release/rust_kzg_blst.a ++TARGETS = ckzg.c ../../../../target/release/rust_kzg_constantine.a CFLAGS += -O2 -Wall -Wextra -shared CFLAGS += ${addprefix -I,${INCLUDE_DIRS}} diff --git a/constantine/go.patch b/constantine/go.patch index dd2de92cc..14183710b 100644 --- a/constantine/go.patch +++ b/constantine/go.patch @@ -24,7 +24,7 @@ index bdd5385..155fc81 100644 +// #endif +// #include +// #include "c_kzg_4844.h" -+// #cgo LDFLAGS: -L${SRCDIR}/../../../../target/release -l:rust_kzg_blst.a -lm ++// #cgo LDFLAGS: -L${SRCDIR}/../../../../target/release -l:rust_kzg_constantine.a -lm import "C" import ( diff --git a/constantine/java.patch b/constantine/java.patch index 978ddb89b..12ca3f38c 100644 --- a/constantine/java.patch +++ b/constantine/java.patch @@ -15,7 +15,7 @@ index 9be2fd6..1e59378 100644 INCLUDE_DIRS = ../../src ../../blst/bindings -TARGETS=c_kzg_4844_jni.c ../../src/c_kzg_4844.c ../../lib/libblst.a -+TARGETS=c_kzg_4844_jni.c ../../../../target/release/rust_kzg_blst.a ++TARGETS=c_kzg_4844_jni.c ../../../../target/release/rust_kzg_constantine.a CC_FLAGS= OPTIMIZATION_LEVEL=-O2 diff --git a/constantine/nodejs.patch b/constantine/nodejs.patch index 3e310fd91..1cf0e3dce 100644 --- a/constantine/nodejs.patch +++ b/constantine/nodejs.patch @@ -65,7 +65,7 @@ index 5ac368e..6cde37f 100644 - } - }] + "libraries": [ -+ "<(module_root_dir)/../../../../target/release/rust_kzg_blst.a" ++ "<(module_root_dir)/../../../../target/release/rust_kzg_constantine.a" ] } ] diff --git a/constantine/python.patch b/constantine/python.patch index c039782de..c9ab361f1 100644 --- a/constantine/python.patch +++ b/constantine/python.patch @@ -39,7 +39,7 @@ index b072833..db37db4 100644 - library_dirs=["../../lib"], - libraries=["blst"])]) + library_dirs=["../../lib", "../../../../target/release"], -+ libraries=[":rust_kzg_blst.a"])]) ++ libraries=[":rust_kzg_constantine.a"])]) if __name__ == "__main__": main() diff --git a/constantine/rust.patch b/constantine/rust.patch index bd3c42a1f..a298fd244 100644 --- a/constantine/rust.patch +++ b/constantine/rust.patch @@ -53,7 +53,7 @@ index 692305a..e874ccd 100644 // Finally, tell cargo this provides ckzg/ckzg_min - println!("cargo:rustc-link-lib=ckzg"); + println!("cargo:rustc-link-search={}", rust_kzg_target_dir.display()); -+ println!("cargo:rustc-link-arg=-l:rust_kzg_blst.a"); ++ println!("cargo:rustc-link-arg=-l:rust_kzg_constantine.a"); } -fn make_bindings

( diff --git a/constantine/src/eip_4844.rs b/constantine/src/eip_4844.rs index 5e7eec72e..ee1131bf1 100644 --- a/constantine/src/eip_4844.rs +++ b/constantine/src/eip_4844.rs @@ -94,6 +94,7 @@ fn kzg_settings_to_rust(c_settings: &CKZGSettings) -> Result>() }, + precomputation: None }) } diff --git a/constantine/src/kzg_proofs.rs b/constantine/src/kzg_proofs.rs index 14dbba05b..d7821f61c 100644 --- a/constantine/src/kzg_proofs.rs +++ b/constantine/src/kzg_proofs.rs @@ -12,6 +12,7 @@ use constantine_sys as constantine; use kzg::G1Affine; use kzg::msm::msm_impls::msm; +use kzg::msm::precompute::PrecomputationTable; use crate::types::g2::CtG2; use blst::{ @@ -27,7 +28,7 @@ impl PairingVerify for CtG1 { } } -pub fn g1_linear_combination(out: &mut CtG1, points: &[CtG1], scalars: &[CtFr], len: usize) { +pub fn g1_linear_combination(out: &mut CtG1, points: &[CtG1], scalars: &[CtFr], len: usize, precomputation: Option<&PrecomputationTable>) { #[cfg(feature = "constantine_msm")] { #[cfg(feature = "parallel")] @@ -58,7 +59,9 @@ pub fn g1_linear_combination(out: &mut CtG1, points: &[CtG1], scalars: &[CtFr], } #[cfg(not(feature = "constantine_msm"))] - *out = msm::(points, scalars, len); + { + *out = msm::(points, scalars, len, precomputation); + } } pub fn pairings_verify(a1: &CtG1, a2: &CtG2, b1: &CtG1, b2: &CtG2) -> bool { diff --git a/constantine/src/mixed_kzg_settings/mixed_kzg_settings.rs b/constantine/src/mixed_kzg_settings/mixed_kzg_settings.rs index 1c8c872e3..5597cdae4 100644 --- a/constantine/src/mixed_kzg_settings/mixed_kzg_settings.rs +++ b/constantine/src/mixed_kzg_settings/mixed_kzg_settings.rs @@ -1,8 +1,8 @@ use std::path::Path; use crate::types::{ - fft_settings::CtFFTSettings, fr::CtFr, g1::CtG1, g2::CtG2, - kzg_settings::CtKZGSettings as GenericContext, poly::CtPoly, + fft_settings::CtFFTSettings, fr::CtFr, g1::{CtG1, CtG1Affine}, g2::CtG2, + kzg_settings::CtKZGSettings as GenericContext, poly::CtPoly, fp::CtFp, }; use constantine_core::Threadpool as CttThreadpool; use constantine_ethereum_kzg::EthKzgContext as CttEthKzgContext; @@ -35,7 +35,7 @@ fn get_thr_count() -> usize { return 1; } -// Constantine requires loading from path, but for UT not always possible +// Constantine requires loading from path + doesn't expose underlying secrets, but sometimes required for tests pub enum MixedKzgSettings { Constantine(CttContext), Generic(GenericContext), @@ -80,15 +80,10 @@ impl MixedKzgSettings { pub fn new_from_path(path: &Path) -> Result { let res = CttEthKzgContext::load_trusted_setup(path); match res { - // #[cfg(feature = "parallel")] Ok(constantine_context) => Ok(Self::Constantine(CttContext { ctx: constantine_context, pool: CttThreadpool::new(get_thr_count()), })), - // #[cfg(not(feature = "parallel"))] - // Ok(constantine_context) => Ok(Self::Constantine(CttContext { - // ctx: constantine_context, - // })), Err(x) => Err(x.to_string()), } } @@ -110,7 +105,7 @@ impl Clone for MixedKzgSettings { } // Allow using MixedKzgSettings as KZGSettings stand-in -impl KZGSettings for MixedKzgSettings { +impl KZGSettings for MixedKzgSettings { fn new( secret_g1: &[CtG1], secret_g2: &[CtG2], @@ -232,4 +227,13 @@ impl KZGSettings for MixedKzgSettings { MixedKzgSettings::Generic(generic_context) => generic_context.get_g2_secret(), } } + + fn get_precomputation(&self) -> Option<&kzg::msm::precompute::PrecomputationTable> { + match self { + MixedKzgSettings::Constantine(constantine_context) => { + panic!("Context not in generic format") + } + MixedKzgSettings::Generic(generic_context) => generic_context.get_precomputation(), + } + } } diff --git a/constantine/src/types/fk20_multi_settings.rs b/constantine/src/types/fk20_multi_settings.rs index 206a703d2..8465308a3 100644 --- a/constantine/src/types/fk20_multi_settings.rs +++ b/constantine/src/types/fk20_multi_settings.rs @@ -14,6 +14,9 @@ use crate::types::g2::CtG2; use crate::types::kzg_settings::CtKZGSettings; use crate::types::poly::CtPoly; +use super::fp::CtFp; +use super::g1::CtG1Affine; + pub struct CtFK20MultiSettings { pub kzg_settings: CtKZGSettings, pub chunk_len: usize, @@ -40,7 +43,7 @@ impl Default for CtFK20MultiSettings { } } -impl FK20MultiSettings +impl FK20MultiSettings for CtFK20MultiSettings { #[allow(clippy::many_single_char_names)] diff --git a/constantine/src/types/fk20_single_settings.rs b/constantine/src/types/fk20_single_settings.rs index 12bdbd747..d0a4776f2 100644 --- a/constantine/src/types/fk20_single_settings.rs +++ b/constantine/src/types/fk20_single_settings.rs @@ -13,13 +13,16 @@ use crate::types::g2::CtG2; use crate::types::kzg_settings::CtKZGSettings; use crate::types::poly::CtPoly; +use super::fp::CtFp; +use super::g1::CtG1Affine; + #[derive(Debug, Clone, Default)] pub struct CtFK20SingleSettings { pub kzg_settings: CtKZGSettings, pub x_ext_fft: Vec, } -impl FK20SingleSettings +impl FK20SingleSettings for CtFK20SingleSettings { fn new(kzg_settings: &CtKZGSettings, n2: usize) -> Result { diff --git a/constantine/src/types/g1.rs b/constantine/src/types/g1.rs index 4aff74a16..2caa49970 100644 --- a/constantine/src/types/g1.rs +++ b/constantine/src/types/g1.rs @@ -3,7 +3,10 @@ extern crate alloc; use alloc::format; use alloc::string::String; use alloc::string::ToString; +use kzg::G1LinComb; +use kzg::msm::precompute::PrecomputationTable; +use crate::kzg_proofs::g1_linear_combination; use crate::types::fp::CtFp; use crate::types::fr::CtFr; use kzg::common_utils::log_2_byte; @@ -221,10 +224,14 @@ impl G1Mul for CtG1 { } result } +} - fn g1_lincomb(points: &[Self], scalars: &[CtFr], len: usize) -> Self { +impl G1LinComb for CtG1 { + fn g1_lincomb( + points: &[Self], scalars: &[CtFr], len: usize, precomputation: Option<&PrecomputationTable>, + ) -> Self { let mut out = CtG1::default(); - crate::kzg_proofs::g1_linear_combination(&mut out, points, scalars, len); + g1_linear_combination(&mut out, points, scalars, len, precomputation); out } } diff --git a/constantine/src/types/kzg_settings.rs b/constantine/src/types/kzg_settings.rs index 4c64abb2f..97d715f71 100644 --- a/constantine/src/types/kzg_settings.rs +++ b/constantine/src/types/kzg_settings.rs @@ -3,6 +3,7 @@ extern crate alloc; use alloc::string::String; use alloc::vec::Vec; +use kzg::msm::precompute::{PrecomputationTable, precompute}; use kzg::{FFTFr, FFTSettings, Fr, G1Mul, G2Mul, KZGSettings, Poly, G1, G2}; use crate::consts::{G1_GENERATOR, G2_GENERATOR}; @@ -13,14 +14,18 @@ use crate::types::g1::CtG1; use crate::types::g2::CtG2; use crate::types::poly::CtPoly; +use super::fp::CtFp; +use super::g1::CtG1Affine; + #[derive(Debug, Clone, Default)] pub struct CtKZGSettings { pub fs: CtFFTSettings, pub secret_g1: Vec, pub secret_g2: Vec, + pub precomputation: Option>, } -impl KZGSettings for CtKZGSettings { +impl KZGSettings for CtKZGSettings { fn new( secret_g1: &[CtG1], secret_g2: &[CtG2], @@ -31,6 +36,7 @@ impl KZGSettings for CtKZGSettings { secret_g1: secret_g1.to_vec(), secret_g2: secret_g2.to_vec(), fs: fft_settings.clone(), + precomputation: precompute(secret_g1).ok().flatten() }) } @@ -40,7 +46,7 @@ impl KZGSettings for CtKZGSettings { } let mut out = CtG1::default(); - g1_linear_combination(&mut out, &self.secret_g1, &poly.coeffs, poly.coeffs.len()); + g1_linear_combination(&mut out, &self.secret_g1, &poly.coeffs, poly.coeffs.len(), self.get_precomputation()); Ok(out) } @@ -188,4 +194,8 @@ impl KZGSettings for CtKZGSettings { fn get_g2_secret(&self) -> &[CtG2] { &self.secret_g2 } + + fn get_precomputation(&self) -> Option<&PrecomputationTable> { + self.precomputation.as_ref() + } } diff --git a/constantine/tests/batch_adder.rs b/constantine/tests/batch_adder.rs index 46a1e3b94..f42908e19 100644 --- a/constantine/tests/batch_adder.rs +++ b/constantine/tests/batch_adder.rs @@ -10,7 +10,7 @@ mod tests { fp::CtFp, g1::{CtG1, CtG1Affine}, }; - // use rust_kzg_blst::types:: + // use rust_kzg_constantine::types:: #[test] fn test_phase_one_zero_or_neg_() { diff --git a/constantine/tests/bls12_381.rs b/constantine/tests/bls12_381.rs index 816c6e334..cb0743ced 100644 --- a/constantine/tests/bls12_381.rs +++ b/constantine/tests/bls12_381.rs @@ -10,8 +10,9 @@ mod tests { }; use rust_kzg_constantine::kzg_proofs::{g1_linear_combination, pairings_verify}; + use rust_kzg_constantine::types::fp::CtFp; use rust_kzg_constantine::types::fr::CtFr; - use rust_kzg_constantine::types::g1::CtG1; + use rust_kzg_constantine::types::g1::{CtG1, CtG1Affine}; use rust_kzg_constantine::types::g2::CtG2; #[test] @@ -106,12 +107,12 @@ mod tests { #[test] fn g1_make_linear_combination_() { - g1_make_linear_combination::(&g1_linear_combination) + g1_make_linear_combination::(&g1_linear_combination) } #[test] fn g1_random_linear_combination_() { - g1_random_linear_combination::(&g1_linear_combination) + g1_random_linear_combination::(&g1_linear_combination) } #[test] diff --git a/constantine/tests/eip_4844.rs b/constantine/tests/eip_4844.rs index 4ec77904a..f6565413e 100644 --- a/constantine/tests/eip_4844.rs +++ b/constantine/tests/eip_4844.rs @@ -27,8 +27,9 @@ mod tests { use rust_kzg_constantine::consts::SCALE2_ROOT_OF_UNITY; use rust_kzg_constantine::eip_4844::load_trusted_setup_filename_rust; use rust_kzg_constantine::types::fft_settings::expand_root_of_unity; + use rust_kzg_constantine::types::g1::CtG1Affine; use rust_kzg_constantine::types::{ - fft_settings::CtFFTSettings, fr::CtFr, g1::CtG1, g2::CtG2, kzg_settings::CtKZGSettings, + fft_settings::CtFFTSettings, fr::CtFr, g1::CtG1, g2::CtG2, kzg_settings::CtKZGSettings, fp::CtFp, poly::CtPoly, }; @@ -44,7 +45,7 @@ mod tests { #[test] pub fn blob_to_kzg_commitment_test_() { - blob_to_kzg_commitment_test::( + blob_to_kzg_commitment_test::( &load_trusted_setup_filename_rust, &blob_to_kzg_commitment_rust, ); @@ -52,7 +53,7 @@ mod tests { #[test] pub fn compute_kzg_proof_test_() { - compute_kzg_proof_test::( + compute_kzg_proof_test::( &load_trusted_setup_filename_rust, &compute_kzg_proof_rust, &blob_to_polynomial, @@ -68,7 +69,7 @@ mod tests { CtG2, CtPoly, CtFFTSettings, - CtKZGSettings, + CtKZGSettings, CtFp, CtG1Affine >( &load_trusted_setup_filename_rust, &blob_to_kzg_commitment_rust, @@ -88,7 +89,7 @@ mod tests { CtG2, CtPoly, CtFFTSettings, - CtKZGSettings, + CtKZGSettings, CtFp, CtG1Affine >( &load_trusted_setup_filename_rust, &blob_to_kzg_commitment_rust, @@ -108,7 +109,7 @@ mod tests { CtG2, CtPoly, CtFFTSettings, - CtKZGSettings, + CtKZGSettings, CtFp, CtG1Affine >( &load_trusted_setup_filename_rust, &blob_to_kzg_commitment_rust, @@ -128,7 +129,7 @@ mod tests { CtG2, CtPoly, CtFFTSettings, - CtKZGSettings, + CtKZGSettings, CtFp, CtG1Affine >( &load_trusted_setup_filename_rust, &blob_to_kzg_commitment_rust, @@ -146,7 +147,7 @@ mod tests { CtG2, CtPoly, CtFFTSettings, - CtKZGSettings, + CtKZGSettings, CtFp, CtG1Affine >( &load_trusted_setup_filename_rust, &blob_to_kzg_commitment_rust, @@ -158,7 +159,7 @@ mod tests { #[test] pub fn verify_kzg_proof_batch_test_() { - verify_kzg_proof_batch_test::( + verify_kzg_proof_batch_test::( &load_trusted_setup_filename_rust, &blob_to_kzg_commitment_rust, &bytes_to_blob, @@ -175,7 +176,7 @@ mod tests { CtG2, CtPoly, CtFFTSettings, - CtKZGSettings, + CtKZGSettings, CtFp, CtG1Affine >( &load_trusted_setup_filename_rust, &blob_to_kzg_commitment_rust, @@ -187,7 +188,7 @@ mod tests { #[test] pub fn test_vectors_blob_to_kzg_commitment_() { - test_vectors_blob_to_kzg_commitment::( + test_vectors_blob_to_kzg_commitment::( &load_trusted_setup_filename_rust, &blob_to_kzg_commitment_rust, &bytes_to_blob, @@ -196,7 +197,7 @@ mod tests { #[test] pub fn test_vectors_compute_kzg_proof_() { - test_vectors_compute_kzg_proof::( + test_vectors_compute_kzg_proof::( &load_trusted_setup_filename_rust, &compute_kzg_proof_rust, &bytes_to_blob, @@ -205,7 +206,7 @@ mod tests { #[test] pub fn test_vectors_compute_blob_kzg_proof_() { - test_vectors_compute_blob_kzg_proof::( + test_vectors_compute_blob_kzg_proof::( &load_trusted_setup_filename_rust, &bytes_to_blob, &compute_blob_kzg_proof_rust, @@ -214,7 +215,7 @@ mod tests { #[test] pub fn test_vectors_verify_kzg_proof_() { - test_vectors_verify_kzg_proof::( + test_vectors_verify_kzg_proof::( &load_trusted_setup_filename_rust, &verify_kzg_proof_rust, ); @@ -222,7 +223,7 @@ mod tests { #[test] pub fn test_vectors_verify_blob_kzg_proof_() { - test_vectors_verify_blob_kzg_proof::( + test_vectors_verify_blob_kzg_proof::( &load_trusted_setup_filename_rust, &bytes_to_blob, &verify_blob_kzg_proof_rust, @@ -237,7 +238,7 @@ mod tests { CtG2, CtPoly, CtFFTSettings, - CtKZGSettings, + CtKZGSettings, CtFp, CtG1Affine >( &load_trusted_setup_filename_rust, &bytes_to_blob, @@ -270,7 +271,7 @@ mod tests { CtG1, CtG2, CtFFTSettings, - CtKZGSettings, + CtKZGSettings, CtFp, CtG1Affine >(&evaluate_polynomial_in_evaluation_form); } @@ -282,7 +283,7 @@ mod tests { CtG1, CtG2, CtFFTSettings, - CtKZGSettings, + CtKZGSettings, CtFp, CtG1Affine >(&verify_blob_kzg_proof_batch_rust) } @@ -294,7 +295,7 @@ mod tests { CtG1, CtG2, CtFFTSettings, - CtKZGSettings, + CtKZGSettings, CtFp, CtG1Affine >(&verify_blob_kzg_proof_batch_rust) } @@ -306,13 +307,13 @@ mod tests { CtG1, CtG2, CtFFTSettings, - CtKZGSettings, + CtKZGSettings, CtFp, CtG1Affine >(&verify_blob_kzg_proof_batch_rust) } #[test] pub fn validate_batched_input() { - validate_batched_input_test::( + validate_batched_input_test::( &verify_blob_kzg_proof_batch_rust, &load_trusted_setup_filename_rust, ) diff --git a/constantine/tests/eip_4844_constantine.rs b/constantine/tests/eip_4844_constantine.rs index 92aa2b788..ddfb5a15d 100644 --- a/constantine/tests/eip_4844_constantine.rs +++ b/constantine/tests/eip_4844_constantine.rs @@ -36,6 +36,8 @@ mod tests { }; use rust_kzg_constantine::mixed_kzg_settings::mixed_kzg_settings::MixedKzgSettings; use rust_kzg_constantine::types::fft_settings::expand_root_of_unity; + use rust_kzg_constantine::types::fp::CtFp; + use rust_kzg_constantine::types::g1::CtG1Affine; use rust_kzg_constantine::types::{ fft_settings::CtFFTSettings, fr::CtFr, g1::CtG1, g2::CtG2, kzg_settings::CtKZGSettings, poly::CtPoly, @@ -53,7 +55,7 @@ mod tests { #[test] pub fn blob_to_kzg_commitment_test_() { - blob_to_kzg_commitment_test::( + blob_to_kzg_commitment_test::( &load_trusted_setup_filename_mixed, &blob_to_kzg_commitment_mixed, ); @@ -61,7 +63,7 @@ mod tests { // #[test] // pub fn compute_kzg_proof_test_() { - // compute_kzg_proof_test::( + // compute_kzg_proof_test::( // &load_trusted_setup_filename_mixed, // &compute_kzg_proof_mixed, // &blob_to_polynomial, @@ -77,8 +79,7 @@ mod tests { // CtG2, // CtPoly, // CtFFTSettings, - // MixedKzgSettings, - // >( + // MixedKzgSettings, CtFp, CtG1Affine>( // &load_trusted_setup_filename_mixed, // &blob_to_kzg_commitment_mixed, // &bytes_to_blob, @@ -97,8 +98,7 @@ mod tests { // CtG2, // CtPoly, // CtFFTSettings, - // MixedKzgSettings, - // >( + // MixedKzgSettings, CtFp, CtG1Affine>( // &load_trusted_setup_filename_mixed, // &blob_to_kzg_commitment_mixed, // &bytes_to_blob, @@ -117,8 +117,7 @@ mod tests { // CtG2, // CtPoly, // CtFFTSettings, - // MixedKzgSettings, - // >( + // MixedKzgSettings, CtFp, CtG1Affine>( // &load_trusted_setup_filename_mixed, // &blob_to_kzg_commitment_mixed, // &bytes_to_blob, @@ -137,8 +136,7 @@ mod tests { CtG2, CtPoly, CtFFTSettings, - MixedKzgSettings, - >( + MixedKzgSettings, CtFp, CtG1Affine>( &load_trusted_setup_filename_mixed, &blob_to_kzg_commitment_mixed, &bytes_to_blob, @@ -155,8 +153,7 @@ mod tests { CtG2, CtPoly, CtFFTSettings, - MixedKzgSettings, - >( + MixedKzgSettings, CtFp, CtG1Affine>( &load_trusted_setup_filename_mixed, &blob_to_kzg_commitment_mixed, &bytes_to_blob, @@ -167,7 +164,7 @@ mod tests { #[test] pub fn verify_kzg_proof_batch_test_() { - verify_kzg_proof_batch_test::( + verify_kzg_proof_batch_test::( &load_trusted_setup_filename_mixed, &blob_to_kzg_commitment_mixed, &bytes_to_blob, @@ -184,8 +181,7 @@ mod tests { CtG2, CtPoly, CtFFTSettings, - MixedKzgSettings, - >( + MixedKzgSettings, CtFp, CtG1Affine>( &load_trusted_setup_filename_mixed, &blob_to_kzg_commitment_mixed, &bytes_to_blob, @@ -202,8 +198,7 @@ mod tests { CtG2, CtPoly, CtFFTSettings, - MixedKzgSettings, - >( + MixedKzgSettings, CtFp, CtG1Affine>( &load_trusted_setup_filename_mixed, &blob_to_kzg_commitment_mixed, &bytes_to_blob, @@ -212,7 +207,7 @@ mod tests { #[test] pub fn test_vectors_compute_kzg_proof_() { - test_vectors_compute_kzg_proof::( + test_vectors_compute_kzg_proof::( &load_trusted_setup_filename_mixed, &compute_kzg_proof_mixed, &bytes_to_blob, @@ -227,8 +222,7 @@ mod tests { CtG2, CtPoly, CtFFTSettings, - MixedKzgSettings, - >( + MixedKzgSettings, CtFp, CtG1Affine> ( &load_trusted_setup_filename_mixed, &bytes_to_blob, &compute_blob_kzg_proof_mixed, @@ -237,7 +231,7 @@ mod tests { #[test] pub fn test_vectors_verify_kzg_proof_() { - test_vectors_verify_kzg_proof::( + test_vectors_verify_kzg_proof::( &load_trusted_setup_filename_mixed, &verify_kzg_proof_mixed, ); @@ -251,8 +245,7 @@ mod tests { CtG2, CtPoly, CtFFTSettings, - MixedKzgSettings, - >( + MixedKzgSettings, CtFp, CtG1Affine>( &load_trusted_setup_filename_mixed, &bytes_to_blob, &verify_blob_kzg_proof_mixed, @@ -267,8 +260,7 @@ mod tests { CtG2, CtPoly, CtFFTSettings, - MixedKzgSettings, - >( + MixedKzgSettings, CtFp, CtG1Affine>( &load_trusted_setup_filename_mixed, &bytes_to_blob, &verify_blob_kzg_proof_batch_mixed, @@ -300,7 +292,7 @@ mod tests { CtG1, CtG2, CtFFTSettings, - MixedKzgSettings, + MixedKzgSettings, CtFp, CtG1Affine >(&evaluate_polynomial_in_evaluation_form); } @@ -312,7 +304,7 @@ mod tests { CtG1, CtG2, CtFFTSettings, - MixedKzgSettings, + MixedKzgSettings, CtFp, CtG1Affine >(&verify_blob_kzg_proof_batch_mixed) } @@ -324,7 +316,7 @@ mod tests { CtG1, CtG2, CtFFTSettings, - MixedKzgSettings, + MixedKzgSettings, CtFp, CtG1Affine >(&verify_blob_kzg_proof_batch_mixed) } @@ -336,13 +328,13 @@ mod tests { CtG1, CtG2, CtFFTSettings, - MixedKzgSettings, + MixedKzgSettings, CtFp, CtG1Affine >(&verify_blob_kzg_proof_batch_mixed) } #[test] pub fn validate_batched_input() { - validate_batched_input_test::( + validate_batched_input_test::( &verify_blob_kzg_proof_batch_mixed, &load_trusted_setup_filename_mixed, ) diff --git a/constantine/tests/fk20_proofs.rs b/constantine/tests/fk20_proofs.rs index 9bb24e5aa..491a8261d 100644 --- a/constantine/tests/fk20_proofs.rs +++ b/constantine/tests/fk20_proofs.rs @@ -4,8 +4,9 @@ mod tests { use rust_kzg_constantine::types::fft_settings::CtFFTSettings; use rust_kzg_constantine::types::fk20_multi_settings::CtFK20MultiSettings; use rust_kzg_constantine::types::fk20_single_settings::CtFK20SingleSettings; + use rust_kzg_constantine::types::fp::CtFp; use rust_kzg_constantine::types::fr::CtFr; - use rust_kzg_constantine::types::g1::CtG1; + use rust_kzg_constantine::types::g1::{CtG1, CtG1Affine}; use rust_kzg_constantine::types::g2::CtG2; use rust_kzg_constantine::types::kzg_settings::CtKZGSettings; use rust_kzg_constantine::types::poly::CtPoly; @@ -13,7 +14,7 @@ mod tests { #[test] fn test_fk_single() { - fk_single::( + fk_single::( &generate_trusted_setup, ); } @@ -28,6 +29,7 @@ mod tests { CtFFTSettings, CtKZGSettings, CtFK20SingleSettings, + CtFp, CtG1Affine >(&generate_trusted_setup); } @@ -41,6 +43,7 @@ mod tests { CtFFTSettings, CtKZGSettings, CtFK20MultiSettings, + CtFp, CtG1Affine >(&generate_trusted_setup); } @@ -54,6 +57,7 @@ mod tests { CtFFTSettings, CtKZGSettings, CtFK20MultiSettings, + CtFp, CtG1Affine >(&generate_trusted_setup); } @@ -67,6 +71,7 @@ mod tests { CtFFTSettings, CtKZGSettings, CtFK20MultiSettings, + CtFp, CtG1Affine >(&generate_trusted_setup); } @@ -80,6 +85,7 @@ mod tests { CtFFTSettings, CtKZGSettings, CtFK20MultiSettings, + CtFp, CtG1Affine >(&generate_trusted_setup); } } diff --git a/constantine/tests/kzg_proofs.rs b/constantine/tests/kzg_proofs.rs index eb27fe6bb..5f8929756 100644 --- a/constantine/tests/kzg_proofs.rs +++ b/constantine/tests/kzg_proofs.rs @@ -10,8 +10,9 @@ mod tests { }; use rust_kzg_constantine::types::fft_settings::CtFFTSettings; + use rust_kzg_constantine::types::fp::CtFp; use rust_kzg_constantine::types::fr::CtFr; - use rust_kzg_constantine::types::g1::CtG1; + use rust_kzg_constantine::types::g1::{CtG1, CtG1Affine}; use rust_kzg_constantine::types::g2::CtG2; use rust_kzg_constantine::types::kzg_settings::CtKZGSettings; use rust_kzg_constantine::types::poly::CtPoly; @@ -19,28 +20,28 @@ mod tests { #[test] pub fn test_proof_single() { - proof_single::( + proof_single::( &generate_trusted_setup, ); } #[test] pub fn test_commit_to_nil_poly() { - commit_to_nil_poly::( + commit_to_nil_poly::( &generate_trusted_setup, ); } #[test] pub fn test_commit_to_too_long_poly() { - commit_to_too_long_poly_returns_err::( + commit_to_too_long_poly_returns_err::( &generate_trusted_setup, ); } #[test] pub fn test_proof_multi() { - proof_multi::( + proof_multi::( &generate_trusted_setup, ); } From 7aef5bc7071e819d037e6825933043e3cb0068de Mon Sep 17 00:00:00 2001 From: Domas Kalinauskas Date: Sun, 31 Dec 2023 12:35:27 +0200 Subject: [PATCH 06/17] remove additional blst usage in g1,g2,fr where possible + clippy fixes --- constantine/benches/eip_4844.rs | 9 +- constantine/benches/eip_4844_constantine.rs | 19 +-- .../benches/eip_4844_constantine_no_conv.rs | 156 +++++++++--------- constantine/benches/fk_20.rs | 30 +++- constantine/benches/kzg.rs | 14 +- constantine/src/consts.rs | 4 +- constantine/src/eip_4844.rs | 4 +- constantine/src/kzg_proofs.rs | 51 ++++-- constantine/src/lib.rs | 2 +- .../mixed_eip_4844.rs | 58 +++---- .../mixed_kzg_settings.rs | 55 +++--- .../{mixed_kzg_settings => mixed_kzg}/mod.rs | 0 constantine/src/types/fr.rs | 47 +++--- constantine/src/types/g1.rs | 56 ++++--- constantine/src/types/g2.rs | 24 +-- constantine/src/types/kzg_settings.rs | 12 +- constantine/src/utils.rs | 12 ++ constantine/tests/eip_4844.rs | 150 ++++++++++++++--- constantine/tests/eip_4844_constantine.rs | 130 +++++++++++---- constantine/tests/fk20_proofs.rs | 29 +++- constantine/tests/kzg_proofs.rs | 32 ++-- kzg-bench/src/lib.rs | 6 +- 22 files changed, 569 insertions(+), 331 deletions(-) rename constantine/src/{mixed_kzg_settings => mixed_kzg}/mixed_eip_4844.rs (79%) rename constantine/src/{mixed_kzg_settings => mixed_kzg}/mixed_kzg_settings.rs (78%) rename constantine/src/{mixed_kzg_settings => mixed_kzg}/mod.rs (100%) diff --git a/constantine/benches/eip_4844.rs b/constantine/benches/eip_4844.rs index 6ec241ded..cad3bd228 100644 --- a/constantine/benches/eip_4844.rs +++ b/constantine/benches/eip_4844.rs @@ -8,8 +8,13 @@ use kzg_bench::benches::eip_4844::bench_eip_4844; use rust_kzg_constantine::{ eip_4844::load_trusted_setup_filename_rust, types::{ - fft_settings::CtFFTSettings, fr::CtFr, g1::{CtG1, CtG1Affine}, g2::CtG2, kzg_settings::CtKZGSettings, - poly::CtPoly, fp::CtFp, + fft_settings::CtFFTSettings, + fp::CtFp, + fr::CtFr, + g1::{CtG1, CtG1Affine}, + g2::CtG2, + kzg_settings::CtKZGSettings, + poly::CtPoly, }, }; diff --git a/constantine/benches/eip_4844_constantine.rs b/constantine/benches/eip_4844_constantine.rs index 2a6f0b4ba..d0cf5d787 100644 --- a/constantine/benches/eip_4844_constantine.rs +++ b/constantine/benches/eip_4844_constantine.rs @@ -1,16 +1,11 @@ // Same as eip_4844.rs, but using constantine implementations of verification functions use criterion::{criterion_group, criterion_main, Criterion}; -use kzg::eip_4844::{ - blob_to_kzg_commitment_rust, bytes_to_blob, compute_blob_kzg_proof_rust, - compute_kzg_proof_rust, verify_blob_kzg_proof_batch_rust, verify_blob_kzg_proof_rust, - verify_kzg_proof_rust, -}; +use kzg::eip_4844::bytes_to_blob; use kzg_bench::benches::eip_4844::bench_eip_4844; use rust_kzg_constantine::{ - eip_4844::load_trusted_setup_filename_rust, - mixed_kzg_settings::mixed_eip_4844::load_trusted_setup_filename_mixed, - mixed_kzg_settings::{ + mixed_kzg::mixed_eip_4844::load_trusted_setup_filename_mixed, + mixed_kzg::{ mixed_eip_4844::{ blob_to_kzg_commitment_mixed, compute_blob_kzg_proof_mixed, compute_kzg_proof_mixed, verify_blob_kzg_proof_batch_mixed, verify_blob_kzg_proof_mixed, verify_kzg_proof_mixed, @@ -18,8 +13,12 @@ use rust_kzg_constantine::{ mixed_kzg_settings::MixedKzgSettings, }, types::{ - fft_settings::CtFFTSettings, fr::CtFr, g1::{CtG1, CtG1Affine}, g2::CtG2, kzg_settings::CtKZGSettings, - poly::CtPoly, fp::CtFp, + fft_settings::CtFFTSettings, + fp::CtFp, + fr::CtFr, + g1::{CtG1, CtG1Affine}, + g2::CtG2, + poly::CtPoly, }, }; diff --git a/constantine/benches/eip_4844_constantine_no_conv.rs b/constantine/benches/eip_4844_constantine_no_conv.rs index 64d3bb427..cfa31951d 100644 --- a/constantine/benches/eip_4844_constantine_no_conv.rs +++ b/constantine/benches/eip_4844_constantine_no_conv.rs @@ -1,36 +1,15 @@ // Same as eip_4844.rs, but using constantine implementations of verification functions -use std::env::set_current_dir; use std::path::Path; +use criterion::{criterion_group, criterion_main}; +use criterion::{BatchSize, BenchmarkId, Criterion, Throughput}; + +use kzg::eip_4844::{BYTES_PER_BLOB, TRUSTED_SETUP_PATH}; + use kzg_bench::set_trusted_setup_dir; use kzg_bench::tests::eip_4844::{generate_random_blob_bytes, generate_random_field_element_bytes}; -use constantine_ethereum_kzg::EthKzgContext; -use criterion::{BatchSize, BenchmarkId, Criterion, Throughput}; -use kzg::eip_4844::{TRUSTED_SETUP_PATH, BYTES_PER_BLOB}; -use kzg::{FFTSettings, Fr, KZGSettings, Poly, G1, G2}; -use criterion::{criterion_group, criterion_main}; -use kzg::eip_4844::{ - blob_to_kzg_commitment_rust, bytes_to_blob, compute_blob_kzg_proof_rust, - compute_kzg_proof_rust, verify_blob_kzg_proof_batch_rust, verify_blob_kzg_proof_rust, - verify_kzg_proof_rust, -}; -use kzg_bench::benches::eip_4844::bench_eip_4844; -use rust_kzg_constantine::{ - eip_4844::load_trusted_setup_filename_rust, - mixed_kzg_settings::mixed_eip_4844::load_trusted_setup_filename_mixed, - mixed_kzg_settings::{ - mixed_eip_4844::{ - blob_to_kzg_commitment_mixed, compute_blob_kzg_proof_mixed, compute_kzg_proof_mixed, - verify_blob_kzg_proof_batch_mixed, verify_blob_kzg_proof_mixed, verify_kzg_proof_mixed, - }, - mixed_kzg_settings::{MixedKzgSettings, CttContext}, - }, - types::{ - fft_settings::CtFFTSettings, fr::CtFr, g1::CtG1, g2::CtG2, kzg_settings::CtKZGSettings, - poly::CtPoly, - }, -}; +use rust_kzg_constantine::mixed_kzg::mixed_kzg_settings::CttContext; fn bench_eip_4844_constantine_no_conv_(c: &mut Criterion) { set_trusted_setup_dir(); @@ -39,9 +18,7 @@ fn bench_eip_4844_constantine_no_conv_(c: &mut Criterion) { const MAX_COUNT: usize = 64; let blobs: Vec<[u8; BYTES_PER_BLOB]> = (0..MAX_COUNT) - .map(|_| { - generate_random_blob_bytes(&mut rng) - }) + .map(|_| generate_random_blob_bytes(&mut rng)) .collect(); let commitments: Vec<[u8; 48]> = blobs @@ -56,14 +33,15 @@ fn bench_eip_4844_constantine_no_conv_(c: &mut Criterion) { .collect(); let fields: Vec<[u8; 32]> = (0..MAX_COUNT) - .map(|_| { - generate_random_field_element_bytes(&mut rng) - }) + .map(|_| generate_random_field_element_bytes(&mut rng)) .collect(); c.bench_function("blob_to_kzg_commitment", |b| { #[cfg(feature = "parallel")] - b.iter(|| ctx.ctx.blob_to_kzg_commitment_parallel(&ctx.pool, blobs.first().unwrap())); + b.iter(|| { + ctx.ctx + .blob_to_kzg_commitment_parallel(&ctx.pool, blobs.first().unwrap()) + }); #[cfg(not(feature = "parallel"))] b.iter(|| ctx.ctx.blob_to_kzg_commitment(blobs.first().unwrap())); @@ -71,52 +49,73 @@ fn bench_eip_4844_constantine_no_conv_(c: &mut Criterion) { c.bench_function("compute_kzg_proof", |b| { #[cfg(feature = "parallel")] - b.iter(|| ctx.ctx.compute_kzg_proof_parallel(&ctx.pool, blobs.first().unwrap(), fields.first().unwrap())); + b.iter(|| { + ctx.ctx.compute_kzg_proof_parallel( + &ctx.pool, + blobs.first().unwrap(), + fields.first().unwrap(), + ) + }); #[cfg(not(feature = "parallel"))] - b.iter(|| ctx.ctx.compute_kzg_proof(blobs.first().unwrap(), fields.first().unwrap())); + b.iter(|| { + ctx.ctx + .compute_kzg_proof(blobs.first().unwrap(), fields.first().unwrap()) + }); }); c.bench_function("verify_kzg_proof", |b| { b.iter(|| { - ctx.ctx.verify_kzg_proof( - commitments.first().unwrap(), - fields.first().unwrap(), - fields.first().unwrap(), - proofs.first().unwrap(), - ) - .unwrap() + ctx.ctx + .verify_kzg_proof( + commitments.first().unwrap(), + fields.first().unwrap(), + fields.first().unwrap(), + proofs.first().unwrap(), + ) + .unwrap() }) }); c.bench_function("compute_blob_kzg_proof", |b| { #[cfg(feature = "parallel")] - b.iter(|| ctx.ctx.compute_blob_kzg_proof_parallel(&ctx.pool, blobs.first().unwrap(), commitments.first().unwrap())); + b.iter(|| { + ctx.ctx.compute_blob_kzg_proof_parallel( + &ctx.pool, + blobs.first().unwrap(), + commitments.first().unwrap(), + ) + }); #[cfg(not(feature = "parallel"))] - b.iter(|| ctx.ctx.compute_blob_kzg_proof(blobs.first().unwrap(), commitments.first().unwrap())); + b.iter(|| { + ctx.ctx + .compute_blob_kzg_proof(blobs.first().unwrap(), commitments.first().unwrap()) + }); }); c.bench_function("verify_blob_kzg_proof", |b| { #[cfg(feature = "parallel")] b.iter(|| { - ctx.ctx.verify_blob_kzg_proof_parallel( - &ctx.pool, - blobs.first().unwrap(), - commitments.first().unwrap(), - proofs.first().unwrap(), - ) - .unwrap() + ctx.ctx + .verify_blob_kzg_proof_parallel( + &ctx.pool, + blobs.first().unwrap(), + commitments.first().unwrap(), + proofs.first().unwrap(), + ) + .unwrap() }); #[cfg(not(feature = "parallel"))] b.iter(|| { - ctx.ctx.verify_blob_kzg_proof( - blobs.first().unwrap(), - commitments.first().unwrap(), - proofs.first().unwrap(), - ) - .unwrap() + ctx.ctx + .verify_blob_kzg_proof( + blobs.first().unwrap(), + commitments.first().unwrap(), + proofs.first().unwrap(), + ) + .unwrap() }); }); @@ -127,40 +126,37 @@ fn bench_eip_4844_constantine_no_conv_(c: &mut Criterion) { group.bench_with_input(BenchmarkId::from_parameter(count), &count, |b, &count| { b.iter_batched_ref( || { - let blobs_subset = blobs - .clone() - .into_iter() - .take(count) - .collect::>(); + let blobs_subset = blobs.clone().into_iter().take(count).collect::>(); let commitments_subset = commitments .clone() .into_iter() .take(count) .collect::>(); - let proofs_subset = - proofs.clone().into_iter().take(count).collect::>(); + let proofs_subset = proofs.clone().into_iter().take(count).collect::>(); (blobs_subset, commitments_subset, proofs_subset) }, |(blobs_subset, commitments_subset, proofs_subset)| { #[cfg(feature = "parallel")] - ctx.ctx.verify_blob_kzg_proof_batch_parallel( - &ctx.pool, - blobs_subset, - commitments_subset, - proofs_subset, - &rand_thing, - ) - .unwrap(); + ctx.ctx + .verify_blob_kzg_proof_batch_parallel( + &ctx.pool, + blobs_subset, + commitments_subset, + proofs_subset, + &rand_thing, + ) + .unwrap(); #[cfg(not(feature = "parallel"))] - ctx.ctx.verify_blob_kzg_proof_batch( - blobs_subset, - commitments_subset, - proofs_subset, - &rand_thing, - ) - .unwrap(); + ctx.ctx + .verify_blob_kzg_proof_batch( + blobs_subset, + commitments_subset, + proofs_subset, + &rand_thing, + ) + .unwrap(); }, BatchSize::LargeInput, ); diff --git a/constantine/benches/fk_20.rs b/constantine/benches/fk_20.rs index f56a929bc..418159b18 100644 --- a/constantine/benches/fk_20.rs +++ b/constantine/benches/fk_20.rs @@ -13,17 +13,31 @@ use rust_kzg_constantine::types::poly::CtPoly; use rust_kzg_constantine::utils::generate_trusted_setup; fn bench_fk_single_da_(c: &mut Criterion) { - bench_fk_single_da::( - c, - &generate_trusted_setup, - ) + bench_fk_single_da::< + CtFr, + CtG1, + CtG2, + CtPoly, + CtFFTSettings, + CtKZGSettings, + CtFK20SingleSettings, + CtFp, + CtG1Affine, + >(c, &generate_trusted_setup) } fn bench_fk_multi_da_(c: &mut Criterion) { - bench_fk_multi_da::( - c, - &generate_trusted_setup, - ) + bench_fk_multi_da::< + CtFr, + CtG1, + CtG2, + CtPoly, + CtFFTSettings, + CtKZGSettings, + CtFK20MultiSettings, + CtFp, + CtG1Affine, + >(c, &generate_trusted_setup) } criterion_group! { diff --git a/constantine/benches/kzg.rs b/constantine/benches/kzg.rs index 7985c8414..32a1b9c8e 100644 --- a/constantine/benches/kzg.rs +++ b/constantine/benches/kzg.rs @@ -17,10 +17,16 @@ fn bench_commit_to_poly_(c: &mut Criterion) { } fn bench_compute_proof_single_(c: &mut Criterion) { - bench_compute_proof_single::( - c, - &generate_trusted_setup, - ) + bench_compute_proof_single::< + CtFr, + CtG1, + CtG2, + CtPoly, + CtFFTSettings, + CtKZGSettings, + CtFp, + CtG1Affine, + >(c, &generate_trusted_setup) } criterion_group! { diff --git a/constantine/src/consts.rs b/constantine/src/consts.rs index 755a09897..903165e19 100644 --- a/constantine/src/consts.rs +++ b/constantine/src/consts.rs @@ -1,7 +1,5 @@ //blst_fp = bls12_381_fp, CtG1 = CtG1, blst_p1 = bls12_381_g1_jac, blst_fr = bls12_381_fr -use constantine_sys::{ - bls12_381_fp, bls12_381_fp2, bls12_381_g1_aff, bls12_381_g1_jac, bls12_381_g2_jac, -}; +use constantine_sys::{bls12_381_fp, bls12_381_fp2, bls12_381_g1_jac, bls12_381_g2_jac}; use crate::types::g1::CtG1; use crate::types::g2::CtG2; diff --git a/constantine/src/eip_4844.rs b/constantine/src/eip_4844.rs index ee1131bf1..2dd70889f 100644 --- a/constantine/src/eip_4844.rs +++ b/constantine/src/eip_4844.rs @@ -34,8 +34,6 @@ use crate::types::g1::CtG1; use crate::types::g2::CtG2; use crate::types::kzg_settings::CtKZGSettings; -use constantine_sys::{bls12_381_fr, bls12_381_g1_jac, bls12_381_g2_jac}; - #[cfg(feature = "parallel")] use rayon::prelude::*; @@ -94,7 +92,7 @@ fn kzg_settings_to_rust(c_settings: &CKZGSettings) -> Result>() }, - precomputation: None + precomputation: None, }) } diff --git a/constantine/src/kzg_proofs.rs b/constantine/src/kzg_proofs.rs index d7821f61c..dabfe0aa2 100644 --- a/constantine/src/kzg_proofs.rs +++ b/constantine/src/kzg_proofs.rs @@ -4,21 +4,23 @@ use crate::types::fp::CtFp; use crate::types::g1::CtG1; use crate::types::{fr::CtFr, g1::CtG1Affine}; -use crate::types::g1::CtG1ProjAddAffine; +use crate::utils::{ptr_transmute, ptr_transmute_mut}; +#[cfg(feature = "constantine_msm")] use constantine_sys as constantine; -// use constantine_ethereum_kzg::Threadpool as constantine_pool; - +#[cfg(feature = "constantine_msm")] use kzg::G1Affine; +#[cfg(not(feature = "constantine_msm"))] use kzg::msm::msm_impls::msm; + +#[cfg(not(feature = "constantine_msm"))] +use crate::types::g1::CtG1ProjAddAffine; + use kzg::msm::precompute::PrecomputationTable; use crate::types::g2::CtG2; -use blst::{ - blst_fp12_is_one, blst_p1_affine, blst_p1_cneg, blst_p1_to_affine, blst_p2_affine, - blst_p2_to_affine, Pairing, -}; +use blst::{blst_p1_affine, blst_p1_cneg, blst_p1_to_affine, blst_p2_affine, blst_p2_to_affine}; use kzg::PairingVerify; @@ -28,12 +30,24 @@ impl PairingVerify for CtG1 { } } -pub fn g1_linear_combination(out: &mut CtG1, points: &[CtG1], scalars: &[CtFr], len: usize, precomputation: Option<&PrecomputationTable>) { +pub fn g1_linear_combination( + out: &mut CtG1, + points: &[CtG1], + scalars: &[CtFr], + len: usize, + _precomputation: Option<&PrecomputationTable>, +) { #[cfg(feature = "constantine_msm")] { + if len == 0 { + *out = CtG1::default(); + return; + } + #[cfg(feature = "parallel")] - let pool = - unsafe { constantine::ctt_threadpool_new(constantine_sys::ctt_cpu_get_num_threads_os()) }; + let pool = unsafe { + constantine::ctt_threadpool_new(constantine_sys::ctt_cpu_get_num_threads_os()) + }; #[cfg(not(feature = "parallel"))] let pool = unsafe { constantine::ctt_threadpool_new(1) }; @@ -60,7 +74,12 @@ pub fn g1_linear_combination(out: &mut CtG1, points: &[CtG1], scalars: &[CtFr], #[cfg(not(feature = "constantine_msm"))] { - *out = msm::(points, scalars, len, precomputation); + *out = msm::( + points, + scalars, + len, + _precomputation, + ); } } @@ -76,12 +95,12 @@ pub fn pairings_verify(a1: &CtG1, a2: &CtG2, b1: &CtG1, b2: &CtG2) -> bool { // so we negate one of the points. let mut a1neg: CtG1 = *a1; unsafe { - blst_p1_cneg(core::mem::transmute(&mut a1neg.0), true); - blst_p1_to_affine(&mut aa1, core::mem::transmute(&a1neg.0)); + blst_p1_cneg(ptr_transmute_mut(&mut a1neg.0), true); + blst_p1_to_affine(&mut aa1, ptr_transmute(&a1neg.0)); - blst_p1_to_affine(&mut bb1, core::mem::transmute(&b1.0)); - blst_p2_to_affine(&mut aa2, core::mem::transmute(&a2.0)); - blst_p2_to_affine(&mut bb2, core::mem::transmute(&b2.0)); + blst_p1_to_affine(&mut bb1, ptr_transmute(&b1.0)); + blst_p2_to_affine(&mut aa2, ptr_transmute(&a2.0)); + blst_p2_to_affine(&mut bb2, ptr_transmute(&b2.0)); let dst = [0u8; 3]; let mut pairing_blst = blst::Pairing::new(false, &dst); diff --git a/constantine/src/lib.rs b/constantine/src/lib.rs index 9bc9424ba..265cc11d2 100644 --- a/constantine/src/lib.rs +++ b/constantine/src/lib.rs @@ -7,7 +7,7 @@ pub mod fft_fr; pub mod fft_g1; pub mod fk20_proofs; pub mod kzg_proofs; -pub mod mixed_kzg_settings; +pub mod mixed_kzg; pub mod recovery; pub mod types; pub mod utils; diff --git a/constantine/src/mixed_kzg_settings/mixed_eip_4844.rs b/constantine/src/mixed_kzg/mixed_eip_4844.rs similarity index 79% rename from constantine/src/mixed_kzg_settings/mixed_eip_4844.rs rename to constantine/src/mixed_kzg/mixed_eip_4844.rs index 1f1a0c3d4..f8758a933 100644 --- a/constantine/src/mixed_kzg_settings/mixed_eip_4844.rs +++ b/constantine/src/mixed_kzg/mixed_eip_4844.rs @@ -1,10 +1,9 @@ use std::path::Path; // use crate:: -use crate::mixed_kzg_settings::mixed_kzg_settings::MixedKzgSettings; +use crate::mixed_kzg::mixed_kzg_settings::MixedKzgSettings; -use crate::types::g1::CtG1Affine; -use crate::types::{fft_settings::CtFFTSettings, fr::CtFr, g1::CtG1, g2::CtG2, poly::CtPoly}; +use crate::types::{fr::CtFr, g1::CtG1}; use kzg::eip_4844::{ blob_to_kzg_commitment_rust, compute_blob_kzg_proof_rust, compute_kzg_proof_rust, @@ -68,7 +67,7 @@ pub fn blob_to_kzg_commitment_mixed( // return blob_to_kzg_commitment_rust(blob, ctt_context); } MixedKzgSettings::Generic(generic_context) => { - return blob_to_kzg_commitment_rust(blob, generic_context); + blob_to_kzg_commitment_rust(blob, generic_context) } } } @@ -81,19 +80,16 @@ pub fn compute_kzg_proof_mixed( match s { MixedKzgSettings::Constantine(ctt_context) => { let blob_bytes = blob_fr_to_byte(blob)?; - unsafe { - let res = ctt_context - .ctx - .compute_kzg_proof(&blob_bytes, &z.to_bytes()); - match res { - // FIXME: Might not need the from_bytes on Fr here - Ok((proof, y)) => Ok((CtG1::from_bytes(&proof)?, CtFr::from_bytes(&y)?)), - Err(x) => Err(x.to_string()), - } + let res = ctt_context + .ctx + .compute_kzg_proof(&blob_bytes, &z.to_bytes()); + match res { + Ok((proof, y)) => Ok((CtG1::from_bytes(&proof)?, CtFr::from_bytes(&y)?)), + Err(x) => Err(x.to_string()), } } MixedKzgSettings::Generic(generic_context) => { - return compute_kzg_proof_rust(blob, z, generic_context); + compute_kzg_proof_rust(blob, z, generic_context) } } } @@ -125,7 +121,7 @@ pub fn compute_blob_kzg_proof_mixed( } } MixedKzgSettings::Generic(generic_context) => { - return compute_blob_kzg_proof_rust(blob, commitment, generic_context); + compute_blob_kzg_proof_rust(blob, commitment, generic_context) } } } @@ -139,21 +135,19 @@ pub fn verify_kzg_proof_mixed( ) -> Result { match s { MixedKzgSettings::Constantine(ctt_context) => { - let res = unsafe { - ctt_context.ctx.verify_kzg_proof( - &commitment.to_bytes(), - &z.to_bytes(), - &y.to_bytes(), - &proof.to_bytes(), - ) - }; + let res = ctt_context.ctx.verify_kzg_proof( + &commitment.to_bytes(), + &z.to_bytes(), + &y.to_bytes(), + &proof.to_bytes(), + ); match res { Ok(x) => Ok(x), Err(x) => Err(x.to_string()), } } MixedKzgSettings::Generic(generic_context) => { - return verify_kzg_proof_rust(commitment, z, y, proof, generic_context); + verify_kzg_proof_rust(commitment, z, y, proof, generic_context) } } } @@ -189,7 +183,7 @@ pub fn verify_blob_kzg_proof_mixed( } } MixedKzgSettings::Generic(generic_context) => { - return verify_blob_kzg_proof_rust(blob, commitment_g1, proof_g1, generic_context); + verify_blob_kzg_proof_rust(blob, commitment_g1, proof_g1, generic_context) } } } @@ -203,13 +197,12 @@ pub fn verify_blob_kzg_proof_batch_mixed( match ts { MixedKzgSettings::Constantine(ctt_context) => { let mut blobs_storage = vec![[0u8; BYTES_PER_BLOB]; blobs.len()]; - for (i, blob) in blobs.into_iter().enumerate() { + for (i, blob) in blobs.iter().enumerate() { let res = blob_fr_to_byte_inplace(blob, &mut blobs_storage[i]); - if res.is_some() { - return Err(res.unwrap()); + if let Some(res) = res { + return Err(res); } } - // let blobs = blobs.iter().map(blob_fr_to_byte_vec).collect::, _>>()?; let commitments = commitments_g1 .iter() @@ -242,12 +235,7 @@ pub fn verify_blob_kzg_proof_batch_mixed( } } MixedKzgSettings::Generic(generic_context) => { - return verify_blob_kzg_proof_batch_rust( - blobs, - commitments_g1, - proofs_g1, - generic_context, - ); + verify_blob_kzg_proof_batch_rust(blobs, commitments_g1, proofs_g1, generic_context) } } } diff --git a/constantine/src/mixed_kzg_settings/mixed_kzg_settings.rs b/constantine/src/mixed_kzg/mixed_kzg_settings.rs similarity index 78% rename from constantine/src/mixed_kzg_settings/mixed_kzg_settings.rs rename to constantine/src/mixed_kzg/mixed_kzg_settings.rs index 5597cdae4..3bff3a1cc 100644 --- a/constantine/src/mixed_kzg_settings/mixed_kzg_settings.rs +++ b/constantine/src/mixed_kzg/mixed_kzg_settings.rs @@ -1,14 +1,21 @@ use std::path::Path; use crate::types::{ - fft_settings::CtFFTSettings, fr::CtFr, g1::{CtG1, CtG1Affine}, g2::CtG2, - kzg_settings::CtKZGSettings as GenericContext, poly::CtPoly, fp::CtFp, + fft_settings::CtFFTSettings, + fp::CtFp, + fr::CtFr, + g1::{CtG1, CtG1Affine}, + g2::CtG2, + kzg_settings::CtKZGSettings as GenericContext, + poly::CtPoly, }; use constantine_core::Threadpool as CttThreadpool; use constantine_ethereum_kzg::EthKzgContext as CttEthKzgContext; use constantine_sys::{ctt_eth_kzg_status, ctt_eth_trusted_setup_status}; use kzg::KZGSettings; +use super::mixed_eip_4844::verify_kzg_proof_mixed; + pub struct CttContext { pub ctx: CttEthKzgContext, pub pool: CttThreadpool, @@ -36,6 +43,7 @@ fn get_thr_count() -> usize { } // Constantine requires loading from path + doesn't expose underlying secrets, but sometimes required for tests +#[allow(clippy::large_enum_variant)] pub enum MixedKzgSettings { Constantine(CttContext), Generic(GenericContext), @@ -98,7 +106,7 @@ impl Default for MixedKzgSettings { impl Clone for MixedKzgSettings { fn clone(&self) -> Self { match self { - Self::Constantine(arg0) => panic!("Cannot clone constantine context"), + Self::Constantine(_) => panic!("Cannot clone constantine context"), Self::Generic(arg0) => Self::Generic(arg0.clone()), } } @@ -117,18 +125,14 @@ impl KZGSettings for fn commit_to_poly(&self, p: &CtPoly) -> Result { match self { - MixedKzgSettings::Constantine(constantine_context) => { - Err("Context not in generic format".to_string()) - } + MixedKzgSettings::Constantine(_) => Err("Context not in generic format".to_string()), MixedKzgSettings::Generic(generic_context) => generic_context.commit_to_poly(p), } } fn compute_proof_single(&self, p: &CtPoly, x: &CtFr) -> Result { match self { - MixedKzgSettings::Constantine(constantine_context) => { - Err("Context not in generic format".to_string()) - } + MixedKzgSettings::Constantine(_) => Err("Context not in generic format".to_string()), MixedKzgSettings::Generic(generic_context) => { generic_context.compute_proof_single(p, x) } @@ -142,21 +146,12 @@ impl KZGSettings for x: &CtFr, value: &CtFr, ) -> Result { - match self { - MixedKzgSettings::Constantine(constantine_context) => { - Err("Context not in generic format".to_string()) - } - MixedKzgSettings::Generic(generic_context) => { - generic_context.check_proof_single(com, proof, x, value) - } - } + verify_kzg_proof_mixed(com, x, value, proof, self) } fn compute_proof_multi(&self, p: &CtPoly, x: &CtFr, n: usize) -> Result { match self { - MixedKzgSettings::Constantine(constantine_context) => { - Err("Context not in generic format".to_string()) - } + MixedKzgSettings::Constantine(_) => Err("Context not in generic format".to_string()), MixedKzgSettings::Generic(generic_context) => { generic_context.compute_proof_multi(p, x, n) } @@ -172,9 +167,7 @@ impl KZGSettings for n: usize, ) -> Result { match self { - MixedKzgSettings::Constantine(constantine_context) => { - Err("Context not in generic format".to_string()) - } + MixedKzgSettings::Constantine(_) => Err("Context not in generic format".to_string()), MixedKzgSettings::Generic(generic_context) => { generic_context.check_proof_multi(com, proof, x, values, n) } @@ -183,7 +176,7 @@ impl KZGSettings for fn get_expanded_roots_of_unity_at(&self, i: usize) -> CtFr { match self { - MixedKzgSettings::Constantine(constantine_context) => { + MixedKzgSettings::Constantine(_) => { panic!("Context not in generic format") } MixedKzgSettings::Generic(generic_context) => { @@ -194,7 +187,7 @@ impl KZGSettings for fn get_roots_of_unity_at(&self, i: usize) -> CtFr { match self { - MixedKzgSettings::Constantine(constantine_context) => { + MixedKzgSettings::Constantine(_) => { panic!("Context not in generic format") } MixedKzgSettings::Generic(generic_context) => generic_context.get_roots_of_unity_at(i), @@ -203,7 +196,7 @@ impl KZGSettings for fn get_fft_settings(&self) -> &CtFFTSettings { match self { - MixedKzgSettings::Constantine(constantine_context) => { + MixedKzgSettings::Constantine(_) => { panic!("Context not in generic format") } MixedKzgSettings::Generic(generic_context) => generic_context.get_fft_settings(), @@ -212,7 +205,7 @@ impl KZGSettings for fn get_g1_secret(&self) -> &[CtG1] { match self { - MixedKzgSettings::Constantine(constantine_context) => { + MixedKzgSettings::Constantine(_) => { panic!("Context not in generic format") } MixedKzgSettings::Generic(generic_context) => generic_context.get_g1_secret(), @@ -221,16 +214,18 @@ impl KZGSettings for fn get_g2_secret(&self) -> &[CtG2] { match self { - MixedKzgSettings::Constantine(constantine_context) => { + MixedKzgSettings::Constantine(_) => { panic!("Context not in generic format") } MixedKzgSettings::Generic(generic_context) => generic_context.get_g2_secret(), } } - fn get_precomputation(&self) -> Option<&kzg::msm::precompute::PrecomputationTable> { + fn get_precomputation( + &self, + ) -> Option<&kzg::msm::precompute::PrecomputationTable> { match self { - MixedKzgSettings::Constantine(constantine_context) => { + MixedKzgSettings::Constantine(_) => { panic!("Context not in generic format") } MixedKzgSettings::Generic(generic_context) => generic_context.get_precomputation(), diff --git a/constantine/src/mixed_kzg_settings/mod.rs b/constantine/src/mixed_kzg/mod.rs similarity index 100% rename from constantine/src/mixed_kzg_settings/mod.rs rename to constantine/src/mixed_kzg/mod.rs diff --git a/constantine/src/types/fr.rs b/constantine/src/types/fr.rs index 4e4a4a42f..01a16c231 100644 --- a/constantine/src/types/fr.rs +++ b/constantine/src/types/fr.rs @@ -6,16 +6,17 @@ use alloc::string::String; use alloc::string::ToString; use blst::blst_fr; +use constantine::ctt_codec_scalar_status; use kzg::eip_4844::BYTES_PER_FIELD_ELEMENT; use kzg::Fr; use kzg::Scalar256; use constantine_sys as constantine; -use constantine_sys::{ - bls12_381_fr, ctt_bls12_381_fr_cneg_in_place, ctt_bls12_381_fr_diff, ctt_bls12_381_fr_inv, - ctt_bls12_381_fr_prod, ctt_bls12_381_fr_square, ctt_bls12_381_fr_sum, -}; +use constantine_sys::bls12_381_fr; + +use crate::utils::ptr_transmute; +use crate::utils::ptr_transmute_mut; #[repr(C)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] @@ -54,7 +55,7 @@ impl Fr for CtFr { ]; let mut ret = Self::default(); unsafe { - blst::blst_fr_from_uint64(core::mem::transmute(&mut ret.0), val.as_ptr()); + blst::blst_fr_from_uint64(ptr_transmute_mut(&mut ret.0), val.as_ptr()); } ret @@ -74,12 +75,12 @@ impl Fr for CtFr { let mut ret: Self = Self::default(); let mut bls_scalar = blst::blst_scalar::default(); unsafe { - // FIXME: Change to constantine version when available - blst::blst_scalar_from_bendian(&mut bls_scalar, bytes.as_ptr()); - if !blst::blst_scalar_fr_check(&bls_scalar) { + let status = constantine::ctt_bls12_381_deserialize_scalar(ptr_transmute_mut(&mut bls_scalar), bytes.as_ptr()); + if status == ctt_codec_scalar_status::cttCodecScalar_ScalarLargerThanCurveOrder { return Err("Invalid scalar".to_string()); } - blst::blst_fr_from_scalar(core::mem::transmute(&mut ret.0), &bls_scalar); + // FIXME: Change when big255->Fr conversion is available + blst::blst_fr_from_scalar(ptr_transmute_mut(&mut ret.0), &bls_scalar); } Ok(ret) }) @@ -99,8 +100,9 @@ impl Fr for CtFr { let mut ret = Self::default(); let mut bls_scalar = blst::blst_scalar::default(); unsafe { + // FIXME: Seems like no 'non-validating' variant exists in constantine blst::blst_scalar_from_bendian(&mut bls_scalar, bytes.as_ptr()); - blst::blst_fr_from_scalar(core::mem::transmute(&mut ret.0), &bls_scalar); + blst::blst_fr_from_scalar(ptr_transmute_mut(&mut ret.0), &bls_scalar); } ret }) @@ -114,7 +116,8 @@ impl Fr for CtFr { fn from_u64_arr(u: &[u64; 4]) -> Self { let mut ret = Self::default(); unsafe { - blst::blst_fr_from_uint64(core::mem::transmute(&mut ret.0), u.as_ptr()); + // FIXME: Change when big255->Fr conversion is available + blst::blst_fr_from_uint64(ptr_transmute_mut(&mut ret.0), u.as_ptr()); } ret @@ -125,11 +128,12 @@ impl Fr for CtFr { } fn to_bytes(&self) -> [u8; 32] { - let mut scalar = blst::blst_scalar::default(); + let mut scalar = constantine::big255::default(); let mut bytes = [0u8; 32]; unsafe { - blst::blst_scalar_from_fr(&mut scalar, core::mem::transmute(&self.0)); - blst::blst_bendian_from_scalar(bytes.as_mut_ptr(), &scalar); + // FIXME: Change when Fr->Big255 conversion is available + blst::blst_scalar_from_fr(ptr_transmute_mut(&mut scalar), ptr_transmute(&self.0)); + let _ = constantine::ctt_bls12_381_serialize_scalar(bytes.as_mut_ptr(), &scalar); } bytes @@ -138,7 +142,8 @@ impl Fr for CtFr { fn to_u64_arr(&self) -> [u64; 4] { let mut val: [u64; 4] = [0; 4]; unsafe { - blst::blst_uint64_from_fr(val.as_mut_ptr(), core::mem::transmute(&self.0)); + // FIXME: Change when Fr->Big255 conversion is available + blst::blst_uint64_from_fr(val.as_mut_ptr(), ptr_transmute(&self.0)); } val @@ -245,22 +250,14 @@ impl Fr for CtFr { } fn equals(&self, b: &Self) -> bool { - let mut val_a: [u64; 4] = [0; 4]; - let mut val_b: [u64; 4] = [0; 4]; - - unsafe { - constantine::ctt_bls12_381_fr_marshalBE(val_a.as_mut_ptr() as *mut u8, 32, &self.0); - constantine::ctt_bls12_381_fr_marshalBE(val_b.as_mut_ptr() as *mut u8, 32, &b.0); - } - - val_a[0] == val_b[0] && val_a[1] == val_b[1] && val_a[2] == val_b[2] && val_a[3] == val_b[3] + unsafe { constantine::ctt_bls12_381_fr_is_eq(&self.0, &b.0) != 0 } } fn to_scalar(&self) -> kzg::Scalar256 { // FIXME: Change to constantine version when available let mut blst_scalar = blst::blst_scalar::default(); unsafe { - blst::blst_scalar_from_fr(&mut blst_scalar, core::mem::transmute(&self.0)); + blst::blst_scalar_from_fr(&mut blst_scalar, ptr_transmute(&self.0)); } Scalar256::from_u8(&blst_scalar.b) } diff --git a/constantine/src/types/g1.rs b/constantine/src/types/g1.rs index 2caa49970..26d2ed513 100644 --- a/constantine/src/types/g1.rs +++ b/constantine/src/types/g1.rs @@ -3,12 +3,15 @@ extern crate alloc; use alloc::format; use alloc::string::String; use alloc::string::ToString; -use kzg::G1LinComb; +use constantine::ctt_codec_ecc_status; use kzg::msm::precompute::PrecomputationTable; +use kzg::G1LinComb; use crate::kzg_proofs::g1_linear_combination; use crate::types::fp::CtFp; use crate::types::fr::CtFr; +use crate::utils::ptr_transmute; +use crate::utils::ptr_transmute_mut; use kzg::common_utils::log_2_byte; use kzg::eip_4844::BYTES_PER_G1; use kzg::G1Affine; @@ -22,13 +25,11 @@ use crate::consts::{G1_GENERATOR, G1_IDENTITY, G1_NEGATIVE_GENERATOR}; use constantine_sys as constantine; use constantine_sys::{ - bls12_381_fp, bls12_381_g1_aff, bls12_381_g1_jac, ctt_bls12_381_g1_jac_cneg_in_place, - ctt_bls12_381_g1_jac_double, ctt_bls12_381_g1_jac_from_affine, ctt_bls12_381_g1_jac_is_eq, - ctt_bls12_381_g1_jac_is_inf, ctt_bls12_381_g1_jac_sum, + bls12_381_fp, bls12_381_g1_aff, bls12_381_g1_jac, ctt_bls12_381_g1_jac_from_affine, }; #[repr(C)] -#[derive(Debug, Clone, Copy, Default, Eq)] +#[derive(Debug, Clone, Copy, Default)] pub struct CtG1(pub bls12_381_g1_jac); impl PartialEq for CtG1 { @@ -92,10 +93,9 @@ impl G1 for CtG1 { let mut tmp = bls12_381_g1_aff::default(); let mut g1 = bls12_381_g1_jac::default(); unsafe { - let tmp_ref: &mut blst::blst_p1_affine = core::mem::transmute(&mut tmp); // The uncompress routine also checks that the point is on the curve - if blst::blst_p1_uncompress(tmp_ref, bytes.as_ptr()) - != blst::BLST_ERROR::BLST_SUCCESS + let res = constantine::ctt_bls12_381_deserialize_g1_compressed(&mut tmp, bytes.as_ptr()); + if res != ctt_codec_ecc_status::cttCodecEcc_Success && res != ctt_codec_ecc_status::cttCodecEcc_PointAtInfinity { return Err("Failed to uncompress".to_string()); } @@ -113,8 +113,7 @@ impl G1 for CtG1 { fn to_bytes(&self) -> [u8; 48] { let mut out = [0u8; BYTES_PER_G1]; unsafe { - let inp_ref: &blst::blst_p1 = core::mem::transmute(&self.0); - blst::blst_p1_compress(out.as_mut_ptr(), inp_ref); + let _ = constantine::ctt_bls12_381_serialize_g1_compressed(out.as_mut_ptr(), &CtG1Affine::into_affine(&self).0); } out } @@ -133,8 +132,7 @@ impl G1 for CtG1 { fn is_valid(&self) -> bool { unsafe { - // FIXME: Constantine equivalent - blst::blst_p1_in_g1(core::mem::transmute(&self.0)) + constantine::ctt_bls12_381_validate_g1(&CtG1Affine::into_affine(&self).0) == ctt_codec_ecc_status::cttCodecEcc_Success } } @@ -166,10 +164,27 @@ impl G1 for CtG1 { unsafe { constantine::ctt_bls12_381_g1_jac_is_eq(&self.0, &b.0) != 0 } } - // FIXME: Wrong here const ZERO: Self = CtG1::from_xyz( - bls12_381_fp { limbs: [0; 6] }, - bls12_381_fp { limbs: [0; 6] }, + bls12_381_fp { + limbs: [ + 8505329371266088957, + 17002214543764226050, + 6865905132761471162, + 8632934651105793861, + 6631298214892334189, + 1582556514881692819, + ], + }, + bls12_381_fp { + limbs: [ + 8505329371266088957, + 17002214543764226050, + 6865905132761471162, + 8632934651105793861, + 6631298214892334189, + 1582556514881692819, + ], + }, bls12_381_fp { limbs: [0; 6] }, ); @@ -197,7 +212,7 @@ impl G1Mul for CtG1 { // FIXME: No transmute here, use constantine let mut scalar = blst::blst_scalar::default(); unsafe { - blst::blst_scalar_from_fr(&mut scalar, core::mem::transmute(&b.0)); + blst::blst_scalar_from_fr(&mut scalar, ptr_transmute(&b.0)); } // Count the number of bytes to be multiplied. @@ -215,8 +230,8 @@ impl G1Mul for CtG1 { // Count the number of bits to be multiplied. unsafe { blst::blst_p1_mult( - core::mem::transmute(&mut result.0), - core::mem::transmute(&self.0), + ptr_transmute_mut(&mut result.0), + ptr_transmute(&self.0), &(scalar.b[0]), 8 * i - 7 + log_2_byte(scalar.b[i - 1]), ); @@ -228,7 +243,10 @@ impl G1Mul for CtG1 { impl G1LinComb for CtG1 { fn g1_lincomb( - points: &[Self], scalars: &[CtFr], len: usize, precomputation: Option<&PrecomputationTable>, + points: &[Self], + scalars: &[CtFr], + len: usize, + precomputation: Option<&PrecomputationTable>, ) -> Self { let mut out = CtG1::default(); g1_linear_combination(&mut out, points, scalars, len, precomputation); diff --git a/constantine/src/types/g2.rs b/constantine/src/types/g2.rs index e412da30a..10af6d6a9 100644 --- a/constantine/src/types/g2.rs +++ b/constantine/src/types/g2.rs @@ -4,6 +4,7 @@ use alloc::format; use alloc::string::String; use alloc::string::ToString; +use constantine::ctt_codec_ecc_status; use kzg::eip_4844::BYTES_PER_G2; #[cfg(feature = "rand")] use kzg::Fr; @@ -11,16 +12,17 @@ use kzg::{G2Mul, G2}; use crate::consts::{G2_GENERATOR, G2_NEGATIVE_GENERATOR}; use crate::types::fr::CtFr; +use crate::utils::ptr_transmute; +use crate::utils::ptr_transmute_mut; use constantine_sys::{ bls12_381_fp, bls12_381_fp2, bls12_381_g2_aff, bls12_381_g2_jac, - ctt_bls12_381_fp2_double_in_place, ctt_bls12_381_g1_jac_is_eq, - ctt_bls12_381_g2_jac_cneg_in_place, ctt_bls12_381_g2_jac_from_affine, + ctt_bls12_381_g2_jac_from_affine, }; use constantine_sys as constantine; -#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)] +#[derive(Debug, Default, Clone, Copy)] pub struct CtG2(pub bls12_381_g2_jac); impl CtG2 { @@ -100,10 +102,10 @@ impl G2Mul for CtG2 { let mut result = bls12_381_g2_jac::default(); let mut scalar = blst::blst_scalar::default(); unsafe { - blst::blst_scalar_from_fr(&mut scalar, core::mem::transmute(&b.0)); + blst::blst_scalar_from_fr(&mut scalar, ptr_transmute(&b.0)); blst::blst_p2_mult( - core::mem::transmute(&mut result), - core::mem::transmute(&self.0), + ptr_transmute_mut(&mut result), + ptr_transmute(&self.0), scalar.b.as_ptr(), 8 * core::mem::size_of::(), ); @@ -135,10 +137,9 @@ impl G2 for CtG2 { let mut tmp = bls12_381_g2_aff::default(); let mut g2 = bls12_381_g2_jac::default(); unsafe { - let tmp_ref: &mut blst::blst_p2_affine = core::mem::transmute(&mut tmp); // The uncompress routine also checks that the point is on the curve - if blst::blst_p2_uncompress(tmp_ref, bytes.as_ptr()) - != blst::BLST_ERROR::BLST_SUCCESS + let res = constantine::ctt_bls12_381_deserialize_g2_compressed(&mut tmp, bytes.as_ptr()); + if res != ctt_codec_ecc_status::cttCodecEcc_Success && res != ctt_codec_ecc_status::cttCodecEcc_PointAtInfinity { return Err("Failed to uncompress".to_string()); } @@ -150,9 +151,10 @@ impl G2 for CtG2 { fn to_bytes(&self) -> [u8; 96] { let mut out = [0u8; BYTES_PER_G2]; + let mut tmp = bls12_381_g2_aff::default(); unsafe { - let inp_ref: &blst::blst_p2 = core::mem::transmute(&self.0); - blst::blst_p2_compress(out.as_mut_ptr(), inp_ref); + constantine::ctt_bls12_381_g2_jac_affine(&mut tmp, &self.0); + constantine::ctt_bls12_381_serialize_g2_compressed(out.as_mut_ptr(), &tmp); } out } diff --git a/constantine/src/types/kzg_settings.rs b/constantine/src/types/kzg_settings.rs index 97d715f71..0ed954ab8 100644 --- a/constantine/src/types/kzg_settings.rs +++ b/constantine/src/types/kzg_settings.rs @@ -3,7 +3,7 @@ extern crate alloc; use alloc::string::String; use alloc::vec::Vec; -use kzg::msm::precompute::{PrecomputationTable, precompute}; +use kzg::msm::precompute::{precompute, PrecomputationTable}; use kzg::{FFTFr, FFTSettings, Fr, G1Mul, G2Mul, KZGSettings, Poly, G1, G2}; use crate::consts::{G1_GENERATOR, G2_GENERATOR}; @@ -36,7 +36,7 @@ impl KZGSettings for secret_g1: secret_g1.to_vec(), secret_g2: secret_g2.to_vec(), fs: fft_settings.clone(), - precomputation: precompute(secret_g1).ok().flatten() + precomputation: precompute(secret_g1).ok().flatten(), }) } @@ -46,7 +46,13 @@ impl KZGSettings for } let mut out = CtG1::default(); - g1_linear_combination(&mut out, &self.secret_g1, &poly.coeffs, poly.coeffs.len(), self.get_precomputation()); + g1_linear_combination( + &mut out, + &self.secret_g1, + &poly.coeffs, + poly.coeffs.len(), + self.get_precomputation(), + ); Ok(out) } diff --git a/constantine/src/utils.rs b/constantine/src/utils.rs index 3ffa62ec3..a4ab9e8ea 100644 --- a/constantine/src/utils.rs +++ b/constantine/src/utils.rs @@ -25,3 +25,15 @@ pub fn generate_trusted_setup(n: usize, secret: [u8; 32usize]) -> (Vec, Ve (s1, s2) } + +pub fn ptr_transmute(t: &T) -> *const U { + assert_eq!(core::mem::size_of::(), core::mem::size_of::()); + + t as *const T as *const U +} + +pub fn ptr_transmute_mut(t: &mut T) -> *mut U { + assert_eq!(core::mem::size_of::(), core::mem::size_of::()); + + t as *mut T as *mut U +} diff --git a/constantine/tests/eip_4844.rs b/constantine/tests/eip_4844.rs index f6565413e..5c7b44856 100644 --- a/constantine/tests/eip_4844.rs +++ b/constantine/tests/eip_4844.rs @@ -29,8 +29,8 @@ mod tests { use rust_kzg_constantine::types::fft_settings::expand_root_of_unity; use rust_kzg_constantine::types::g1::CtG1Affine; use rust_kzg_constantine::types::{ - fft_settings::CtFFTSettings, fr::CtFr, g1::CtG1, g2::CtG2, kzg_settings::CtKZGSettings, fp::CtFp, - poly::CtPoly, + fft_settings::CtFFTSettings, fp::CtFp, fr::CtFr, g1::CtG1, g2::CtG2, + kzg_settings::CtKZGSettings, poly::CtPoly, }; #[test] @@ -45,7 +45,16 @@ mod tests { #[test] pub fn blob_to_kzg_commitment_test_() { - blob_to_kzg_commitment_test::( + blob_to_kzg_commitment_test::< + CtFr, + CtG1, + CtG2, + CtPoly, + CtFFTSettings, + CtKZGSettings, + CtFp, + CtG1Affine, + >( &load_trusted_setup_filename_rust, &blob_to_kzg_commitment_rust, ); @@ -53,7 +62,16 @@ mod tests { #[test] pub fn compute_kzg_proof_test_() { - compute_kzg_proof_test::( + compute_kzg_proof_test::< + CtFr, + CtG1, + CtG2, + CtPoly, + CtFFTSettings, + CtKZGSettings, + CtFp, + CtG1Affine, + >( &load_trusted_setup_filename_rust, &compute_kzg_proof_rust, &blob_to_polynomial, @@ -69,7 +87,9 @@ mod tests { CtG2, CtPoly, CtFFTSettings, - CtKZGSettings, CtFp, CtG1Affine + CtKZGSettings, + CtFp, + CtG1Affine, >( &load_trusted_setup_filename_rust, &blob_to_kzg_commitment_rust, @@ -89,7 +109,9 @@ mod tests { CtG2, CtPoly, CtFFTSettings, - CtKZGSettings, CtFp, CtG1Affine + CtKZGSettings, + CtFp, + CtG1Affine, >( &load_trusted_setup_filename_rust, &blob_to_kzg_commitment_rust, @@ -109,7 +131,9 @@ mod tests { CtG2, CtPoly, CtFFTSettings, - CtKZGSettings, CtFp, CtG1Affine + CtKZGSettings, + CtFp, + CtG1Affine, >( &load_trusted_setup_filename_rust, &blob_to_kzg_commitment_rust, @@ -129,7 +153,9 @@ mod tests { CtG2, CtPoly, CtFFTSettings, - CtKZGSettings, CtFp, CtG1Affine + CtKZGSettings, + CtFp, + CtG1Affine, >( &load_trusted_setup_filename_rust, &blob_to_kzg_commitment_rust, @@ -147,7 +173,9 @@ mod tests { CtG2, CtPoly, CtFFTSettings, - CtKZGSettings, CtFp, CtG1Affine + CtKZGSettings, + CtFp, + CtG1Affine, >( &load_trusted_setup_filename_rust, &blob_to_kzg_commitment_rust, @@ -159,7 +187,16 @@ mod tests { #[test] pub fn verify_kzg_proof_batch_test_() { - verify_kzg_proof_batch_test::( + verify_kzg_proof_batch_test::< + CtFr, + CtG1, + CtG2, + CtPoly, + CtFFTSettings, + CtKZGSettings, + CtFp, + CtG1Affine, + >( &load_trusted_setup_filename_rust, &blob_to_kzg_commitment_rust, &bytes_to_blob, @@ -176,7 +213,9 @@ mod tests { CtG2, CtPoly, CtFFTSettings, - CtKZGSettings, CtFp, CtG1Affine + CtKZGSettings, + CtFp, + CtG1Affine, >( &load_trusted_setup_filename_rust, &blob_to_kzg_commitment_rust, @@ -188,7 +227,16 @@ mod tests { #[test] pub fn test_vectors_blob_to_kzg_commitment_() { - test_vectors_blob_to_kzg_commitment::( + test_vectors_blob_to_kzg_commitment::< + CtFr, + CtG1, + CtG2, + CtPoly, + CtFFTSettings, + CtKZGSettings, + CtFp, + CtG1Affine, + >( &load_trusted_setup_filename_rust, &blob_to_kzg_commitment_rust, &bytes_to_blob, @@ -197,7 +245,16 @@ mod tests { #[test] pub fn test_vectors_compute_kzg_proof_() { - test_vectors_compute_kzg_proof::( + test_vectors_compute_kzg_proof::< + CtFr, + CtG1, + CtG2, + CtPoly, + CtFFTSettings, + CtKZGSettings, + CtFp, + CtG1Affine, + >( &load_trusted_setup_filename_rust, &compute_kzg_proof_rust, &bytes_to_blob, @@ -206,7 +263,16 @@ mod tests { #[test] pub fn test_vectors_compute_blob_kzg_proof_() { - test_vectors_compute_blob_kzg_proof::( + test_vectors_compute_blob_kzg_proof::< + CtFr, + CtG1, + CtG2, + CtPoly, + CtFFTSettings, + CtKZGSettings, + CtFp, + CtG1Affine, + >( &load_trusted_setup_filename_rust, &bytes_to_blob, &compute_blob_kzg_proof_rust, @@ -215,15 +281,30 @@ mod tests { #[test] pub fn test_vectors_verify_kzg_proof_() { - test_vectors_verify_kzg_proof::( - &load_trusted_setup_filename_rust, - &verify_kzg_proof_rust, - ); + test_vectors_verify_kzg_proof::< + CtFr, + CtG1, + CtG2, + CtPoly, + CtFFTSettings, + CtKZGSettings, + CtFp, + CtG1Affine, + >(&load_trusted_setup_filename_rust, &verify_kzg_proof_rust); } #[test] pub fn test_vectors_verify_blob_kzg_proof_() { - test_vectors_verify_blob_kzg_proof::( + test_vectors_verify_blob_kzg_proof::< + CtFr, + CtG1, + CtG2, + CtPoly, + CtFFTSettings, + CtKZGSettings, + CtFp, + CtG1Affine, + >( &load_trusted_setup_filename_rust, &bytes_to_blob, &verify_blob_kzg_proof_rust, @@ -238,7 +319,9 @@ mod tests { CtG2, CtPoly, CtFFTSettings, - CtKZGSettings, CtFp, CtG1Affine + CtKZGSettings, + CtFp, + CtG1Affine, >( &load_trusted_setup_filename_rust, &bytes_to_blob, @@ -271,7 +354,9 @@ mod tests { CtG1, CtG2, CtFFTSettings, - CtKZGSettings, CtFp, CtG1Affine + CtKZGSettings, + CtFp, + CtG1Affine, >(&evaluate_polynomial_in_evaluation_form); } @@ -283,7 +368,9 @@ mod tests { CtG1, CtG2, CtFFTSettings, - CtKZGSettings, CtFp, CtG1Affine + CtKZGSettings, + CtFp, + CtG1Affine, >(&verify_blob_kzg_proof_batch_rust) } @@ -295,7 +382,9 @@ mod tests { CtG1, CtG2, CtFFTSettings, - CtKZGSettings, CtFp, CtG1Affine + CtKZGSettings, + CtFp, + CtG1Affine, >(&verify_blob_kzg_proof_batch_rust) } @@ -307,13 +396,24 @@ mod tests { CtG1, CtG2, CtFFTSettings, - CtKZGSettings, CtFp, CtG1Affine + CtKZGSettings, + CtFp, + CtG1Affine, >(&verify_blob_kzg_proof_batch_rust) } #[test] pub fn validate_batched_input() { - validate_batched_input_test::( + validate_batched_input_test::< + CtPoly, + CtFr, + CtG1, + CtG2, + CtFFTSettings, + CtKZGSettings, + CtFp, + CtG1Affine, + >( &verify_blob_kzg_proof_batch_rust, &load_trusted_setup_filename_rust, ) diff --git a/constantine/tests/eip_4844_constantine.rs b/constantine/tests/eip_4844_constantine.rs index ddfb5a15d..0c5821526 100644 --- a/constantine/tests/eip_4844_constantine.rs +++ b/constantine/tests/eip_4844_constantine.rs @@ -4,43 +4,36 @@ #[cfg(test)] mod tests { use kzg::eip_4844::{ - blob_to_kzg_commitment_rust, blob_to_polynomial, bytes_to_blob, - compute_blob_kzg_proof_rust, compute_kzg_proof_rust, compute_powers, - evaluate_polynomial_in_evaluation_form, verify_blob_kzg_proof_batch_rust, - verify_blob_kzg_proof_rust, verify_kzg_proof_rust, + blob_to_polynomial, bytes_to_blob, compute_powers, evaluate_polynomial_in_evaluation_form, }; use kzg::Fr; use kzg_bench::tests::eip_4844::{ blob_to_kzg_commitment_test, bytes_to_bls_field_test, compute_and_verify_blob_kzg_proof_fails_with_incorrect_proof_test, - compute_and_verify_blob_kzg_proof_test, - compute_and_verify_kzg_proof_fails_with_incorrect_proof_test, - compute_and_verify_kzg_proof_round_trip_test, - compute_and_verify_kzg_proof_within_domain_test, compute_kzg_proof_empty_blob_vector_test, + compute_and_verify_blob_kzg_proof_test, compute_kzg_proof_empty_blob_vector_test, compute_kzg_proof_incorrect_blob_length_test, compute_kzg_proof_incorrect_commitments_len_test, compute_kzg_proof_incorrect_poly_length_test, compute_kzg_proof_incorrect_proofs_len_test, - compute_kzg_proof_test, compute_powers_test, test_vectors_blob_to_kzg_commitment, + compute_powers_test, test_vectors_blob_to_kzg_commitment, test_vectors_compute_blob_kzg_proof, test_vectors_compute_kzg_proof, test_vectors_verify_blob_kzg_proof, test_vectors_verify_blob_kzg_proof_batch, test_vectors_verify_kzg_proof, validate_batched_input_test, verify_kzg_proof_batch_fails_with_incorrect_proof_test, verify_kzg_proof_batch_test, }; use rust_kzg_constantine::consts::SCALE2_ROOT_OF_UNITY; - use rust_kzg_constantine::eip_4844::load_trusted_setup_filename_rust; - use rust_kzg_constantine::mixed_kzg_settings::mixed_eip_4844::{ + + use rust_kzg_constantine::mixed_kzg::mixed_eip_4844::{ blob_to_kzg_commitment_mixed, compute_blob_kzg_proof_mixed, compute_kzg_proof_mixed, load_trusted_setup_filename_mixed, verify_blob_kzg_proof_batch_mixed, verify_blob_kzg_proof_mixed, verify_kzg_proof_mixed, }; - use rust_kzg_constantine::mixed_kzg_settings::mixed_kzg_settings::MixedKzgSettings; + use rust_kzg_constantine::mixed_kzg::mixed_kzg_settings::MixedKzgSettings; use rust_kzg_constantine::types::fft_settings::expand_root_of_unity; use rust_kzg_constantine::types::fp::CtFp; use rust_kzg_constantine::types::g1::CtG1Affine; use rust_kzg_constantine::types::{ - fft_settings::CtFFTSettings, fr::CtFr, g1::CtG1, g2::CtG2, kzg_settings::CtKZGSettings, - poly::CtPoly, + fft_settings::CtFFTSettings, fr::CtFr, g1::CtG1, g2::CtG2, poly::CtPoly, }; #[test] @@ -55,7 +48,16 @@ mod tests { #[test] pub fn blob_to_kzg_commitment_test_() { - blob_to_kzg_commitment_test::( + blob_to_kzg_commitment_test::< + CtFr, + CtG1, + CtG2, + CtPoly, + CtFFTSettings, + MixedKzgSettings, + CtFp, + CtG1Affine, + >( &load_trusted_setup_filename_mixed, &blob_to_kzg_commitment_mixed, ); @@ -136,7 +138,10 @@ mod tests { CtG2, CtPoly, CtFFTSettings, - MixedKzgSettings, CtFp, CtG1Affine>( + MixedKzgSettings, + CtFp, + CtG1Affine, + >( &load_trusted_setup_filename_mixed, &blob_to_kzg_commitment_mixed, &bytes_to_blob, @@ -153,7 +158,10 @@ mod tests { CtG2, CtPoly, CtFFTSettings, - MixedKzgSettings, CtFp, CtG1Affine>( + MixedKzgSettings, + CtFp, + CtG1Affine, + >( &load_trusted_setup_filename_mixed, &blob_to_kzg_commitment_mixed, &bytes_to_blob, @@ -164,7 +172,16 @@ mod tests { #[test] pub fn verify_kzg_proof_batch_test_() { - verify_kzg_proof_batch_test::( + verify_kzg_proof_batch_test::< + CtFr, + CtG1, + CtG2, + CtPoly, + CtFFTSettings, + MixedKzgSettings, + CtFp, + CtG1Affine, + >( &load_trusted_setup_filename_mixed, &blob_to_kzg_commitment_mixed, &bytes_to_blob, @@ -181,7 +198,10 @@ mod tests { CtG2, CtPoly, CtFFTSettings, - MixedKzgSettings, CtFp, CtG1Affine>( + MixedKzgSettings, + CtFp, + CtG1Affine, + >( &load_trusted_setup_filename_mixed, &blob_to_kzg_commitment_mixed, &bytes_to_blob, @@ -198,7 +218,10 @@ mod tests { CtG2, CtPoly, CtFFTSettings, - MixedKzgSettings, CtFp, CtG1Affine>( + MixedKzgSettings, + CtFp, + CtG1Affine, + >( &load_trusted_setup_filename_mixed, &blob_to_kzg_commitment_mixed, &bytes_to_blob, @@ -207,7 +230,16 @@ mod tests { #[test] pub fn test_vectors_compute_kzg_proof_() { - test_vectors_compute_kzg_proof::( + test_vectors_compute_kzg_proof::< + CtFr, + CtG1, + CtG2, + CtPoly, + CtFFTSettings, + MixedKzgSettings, + CtFp, + CtG1Affine, + >( &load_trusted_setup_filename_mixed, &compute_kzg_proof_mixed, &bytes_to_blob, @@ -222,7 +254,10 @@ mod tests { CtG2, CtPoly, CtFFTSettings, - MixedKzgSettings, CtFp, CtG1Affine> ( + MixedKzgSettings, + CtFp, + CtG1Affine, + >( &load_trusted_setup_filename_mixed, &bytes_to_blob, &compute_blob_kzg_proof_mixed, @@ -231,10 +266,16 @@ mod tests { #[test] pub fn test_vectors_verify_kzg_proof_() { - test_vectors_verify_kzg_proof::( - &load_trusted_setup_filename_mixed, - &verify_kzg_proof_mixed, - ); + test_vectors_verify_kzg_proof::< + CtFr, + CtG1, + CtG2, + CtPoly, + CtFFTSettings, + MixedKzgSettings, + CtFp, + CtG1Affine, + >(&load_trusted_setup_filename_mixed, &verify_kzg_proof_mixed); } #[test] @@ -245,7 +286,10 @@ mod tests { CtG2, CtPoly, CtFFTSettings, - MixedKzgSettings, CtFp, CtG1Affine>( + MixedKzgSettings, + CtFp, + CtG1Affine, + >( &load_trusted_setup_filename_mixed, &bytes_to_blob, &verify_blob_kzg_proof_mixed, @@ -260,7 +304,10 @@ mod tests { CtG2, CtPoly, CtFFTSettings, - MixedKzgSettings, CtFp, CtG1Affine>( + MixedKzgSettings, + CtFp, + CtG1Affine, + >( &load_trusted_setup_filename_mixed, &bytes_to_blob, &verify_blob_kzg_proof_batch_mixed, @@ -292,7 +339,9 @@ mod tests { CtG1, CtG2, CtFFTSettings, - MixedKzgSettings, CtFp, CtG1Affine + MixedKzgSettings, + CtFp, + CtG1Affine, >(&evaluate_polynomial_in_evaluation_form); } @@ -304,7 +353,9 @@ mod tests { CtG1, CtG2, CtFFTSettings, - MixedKzgSettings, CtFp, CtG1Affine + MixedKzgSettings, + CtFp, + CtG1Affine, >(&verify_blob_kzg_proof_batch_mixed) } @@ -316,7 +367,9 @@ mod tests { CtG1, CtG2, CtFFTSettings, - MixedKzgSettings, CtFp, CtG1Affine + MixedKzgSettings, + CtFp, + CtG1Affine, >(&verify_blob_kzg_proof_batch_mixed) } @@ -328,13 +381,24 @@ mod tests { CtG1, CtG2, CtFFTSettings, - MixedKzgSettings, CtFp, CtG1Affine + MixedKzgSettings, + CtFp, + CtG1Affine, >(&verify_blob_kzg_proof_batch_mixed) } #[test] pub fn validate_batched_input() { - validate_batched_input_test::( + validate_batched_input_test::< + CtPoly, + CtFr, + CtG1, + CtG2, + CtFFTSettings, + MixedKzgSettings, + CtFp, + CtG1Affine, + >( &verify_blob_kzg_proof_batch_mixed, &load_trusted_setup_filename_mixed, ) diff --git a/constantine/tests/fk20_proofs.rs b/constantine/tests/fk20_proofs.rs index 491a8261d..6e138c90b 100644 --- a/constantine/tests/fk20_proofs.rs +++ b/constantine/tests/fk20_proofs.rs @@ -14,9 +14,17 @@ mod tests { #[test] fn test_fk_single() { - fk_single::( - &generate_trusted_setup, - ); + fk_single::< + CtFr, + CtG1, + CtG2, + CtPoly, + CtFFTSettings, + CtKZGSettings, + CtFK20SingleSettings, + CtFp, + CtG1Affine, + >(&generate_trusted_setup); } #[test] @@ -29,7 +37,8 @@ mod tests { CtFFTSettings, CtKZGSettings, CtFK20SingleSettings, - CtFp, CtG1Affine + CtFp, + CtG1Affine, >(&generate_trusted_setup); } @@ -43,7 +52,8 @@ mod tests { CtFFTSettings, CtKZGSettings, CtFK20MultiSettings, - CtFp, CtG1Affine + CtFp, + CtG1Affine, >(&generate_trusted_setup); } @@ -57,7 +67,8 @@ mod tests { CtFFTSettings, CtKZGSettings, CtFK20MultiSettings, - CtFp, CtG1Affine + CtFp, + CtG1Affine, >(&generate_trusted_setup); } @@ -71,7 +82,8 @@ mod tests { CtFFTSettings, CtKZGSettings, CtFK20MultiSettings, - CtFp, CtG1Affine + CtFp, + CtG1Affine, >(&generate_trusted_setup); } @@ -85,7 +97,8 @@ mod tests { CtFFTSettings, CtKZGSettings, CtFK20MultiSettings, - CtFp, CtG1Affine + CtFp, + CtG1Affine, >(&generate_trusted_setup); } } diff --git a/constantine/tests/kzg_proofs.rs b/constantine/tests/kzg_proofs.rs index 5f8929756..e7f32d159 100644 --- a/constantine/tests/kzg_proofs.rs +++ b/constantine/tests/kzg_proofs.rs @@ -1,10 +1,6 @@ #[cfg(test)] mod tests { - use blst::{ - blst_final_exp, blst_fp12, blst_fp12_mul, blst_miller_loop, blst_p1_affine, blst_p1_cneg, - blst_p1_to_affine, blst_p2_affine, blst_p2_to_affine, Pairing, - }; - use kzg::G1; + use kzg_bench::tests::kzg_proofs::{ commit_to_nil_poly, commit_to_too_long_poly_returns_err, proof_multi, proof_single, }; @@ -27,16 +23,30 @@ mod tests { #[test] pub fn test_commit_to_nil_poly() { - commit_to_nil_poly::( - &generate_trusted_setup, - ); + commit_to_nil_poly::< + CtFr, + CtG1, + CtG2, + CtPoly, + CtFFTSettings, + CtKZGSettings, + CtFp, + CtG1Affine, + >(&generate_trusted_setup); } #[test] pub fn test_commit_to_too_long_poly() { - commit_to_too_long_poly_returns_err::( - &generate_trusted_setup, - ); + commit_to_too_long_poly_returns_err::< + CtFr, + CtG1, + CtG2, + CtPoly, + CtFFTSettings, + CtKZGSettings, + CtFp, + CtG1Affine, + >(&generate_trusted_setup); } #[test] diff --git a/kzg-bench/src/lib.rs b/kzg-bench/src/lib.rs index f8be54e99..f94616d96 100644 --- a/kzg-bench/src/lib.rs +++ b/kzg-bench/src/lib.rs @@ -1,6 +1,4 @@ -use std::{env::set_current_dir, path::Path}; - -use kzg::eip_4844::TRUSTED_SETUP_PATH; +use std::env::set_current_dir; pub mod benches; pub mod test_vectors; @@ -8,4 +6,4 @@ pub mod tests; pub fn set_trusted_setup_dir() { set_current_dir(env!("CARGO_MANIFEST_DIR")).unwrap(); -} \ No newline at end of file +} From 72e7a5b340c09d8a6f467ff52f841d7e18f75dad Mon Sep 17 00:00:00 2001 From: Domas Kalinauskas Date: Sun, 31 Dec 2023 13:24:34 +0200 Subject: [PATCH 07/17] implement debug trait directly, clippy & fmt fixes --- constantine/src/types/fft_settings.rs | 2 +- constantine/src/types/fk20_single_settings.rs | 2 +- constantine/src/types/fp.rs | 16 ++++++- constantine/src/types/fr.rs | 24 ++++++++-- constantine/src/types/g1.rs | 44 ++++++++++++++++--- constantine/src/types/g2.rs | 12 +++-- constantine/src/types/kzg_settings.rs | 2 +- kzg/src/lib.rs | 2 +- 8 files changed, 86 insertions(+), 18 deletions(-) diff --git a/constantine/src/types/fft_settings.rs b/constantine/src/types/fft_settings.rs index f211237b5..596815159 100644 --- a/constantine/src/types/fft_settings.rs +++ b/constantine/src/types/fft_settings.rs @@ -10,7 +10,7 @@ use kzg::{FFTSettings, Fr}; use crate::consts::SCALE2_ROOT_OF_UNITY; use crate::types::fr::CtFr; -#[derive(Debug, Clone)] +#[derive(Clone)] pub struct CtFFTSettings { pub max_width: usize, pub root_of_unity: CtFr, diff --git a/constantine/src/types/fk20_single_settings.rs b/constantine/src/types/fk20_single_settings.rs index d0a4776f2..686524325 100644 --- a/constantine/src/types/fk20_single_settings.rs +++ b/constantine/src/types/fk20_single_settings.rs @@ -16,7 +16,7 @@ use crate::types::poly::CtPoly; use super::fp::CtFp; use super::g1::CtG1Affine; -#[derive(Debug, Clone, Default)] +#[derive(Clone, Default)] pub struct CtFK20SingleSettings { pub kzg_settings: CtKZGSettings, pub x_ext_fft: Vec, diff --git a/constantine/src/types/fp.rs b/constantine/src/types/fp.rs index 58cdbe55a..adbc36191 100644 --- a/constantine/src/types/fp.rs +++ b/constantine/src/types/fp.rs @@ -1,10 +1,24 @@ use constantine_sys as constantine; use constantine_sys::bls12_381_fp; +use core::fmt::{Debug, Formatter}; use kzg::G1Fp; #[repr(C)] -#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)] +#[derive(Default, Clone, Copy)] pub struct CtFp(pub bls12_381_fp); + +impl PartialEq for CtFp { + fn eq(&self, other: &Self) -> bool { + unsafe { constantine::ctt_bls12_381_fp_is_eq(&self.0, &other.0) != 0 } + } +} + +impl Debug for CtFp { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + write!(f, "CtFp({:?})", self.0.limbs) + } +} + impl G1Fp for CtFp { const ONE: Self = Self(bls12_381_fp { limbs: [ diff --git a/constantine/src/types/fr.rs b/constantine/src/types/fr.rs index 01a16c231..bdb696ddd 100644 --- a/constantine/src/types/fr.rs +++ b/constantine/src/types/fr.rs @@ -7,6 +7,7 @@ use alloc::string::ToString; use blst::blst_fr; use constantine::ctt_codec_scalar_status; +use core::fmt::{Debug, Formatter}; use kzg::eip_4844::BYTES_PER_FIELD_ELEMENT; use kzg::Fr; use kzg::Scalar256; @@ -19,9 +20,22 @@ use crate::utils::ptr_transmute; use crate::utils::ptr_transmute_mut; #[repr(C)] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +#[derive(Clone, Copy, Default)] pub struct CtFr(pub bls12_381_fr); +impl Debug for CtFr { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + write!(f, "CtFr({:?})", self.0.limbs) + } +} + +impl PartialEq for CtFr { + fn eq(&self, other: &Self) -> bool { + self.equals(other) + } +} +impl Eq for CtFr {} + impl CtFr { pub fn from_blst_fr(fr: blst::blst_fr) -> Self { Self(bls12_381_fr { limbs: fr.l }) @@ -75,8 +89,12 @@ impl Fr for CtFr { let mut ret: Self = Self::default(); let mut bls_scalar = blst::blst_scalar::default(); unsafe { - let status = constantine::ctt_bls12_381_deserialize_scalar(ptr_transmute_mut(&mut bls_scalar), bytes.as_ptr()); - if status == ctt_codec_scalar_status::cttCodecScalar_ScalarLargerThanCurveOrder { + let status = constantine::ctt_bls12_381_deserialize_scalar( + ptr_transmute_mut(&mut bls_scalar), + bytes.as_ptr(), + ); + if status == ctt_codec_scalar_status::cttCodecScalar_ScalarLargerThanCurveOrder + { return Err("Invalid scalar".to_string()); } // FIXME: Change when big255->Fr conversion is available diff --git a/constantine/src/types/g1.rs b/constantine/src/types/g1.rs index 26d2ed513..13969a332 100644 --- a/constantine/src/types/g1.rs +++ b/constantine/src/types/g1.rs @@ -7,6 +7,8 @@ use constantine::ctt_codec_ecc_status; use kzg::msm::precompute::PrecomputationTable; use kzg::G1LinComb; +use core::fmt::{Debug, Formatter}; + use crate::kzg_proofs::g1_linear_combination; use crate::types::fp::CtFp; use crate::types::fr::CtFr; @@ -29,7 +31,7 @@ use constantine_sys::{ }; #[repr(C)] -#[derive(Debug, Clone, Copy, Default)] +#[derive(Clone, Copy, Default)] pub struct CtG1(pub bls12_381_g1_jac); impl PartialEq for CtG1 { @@ -38,6 +40,16 @@ impl PartialEq for CtG1 { } } +impl Debug for CtG1 { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + write!( + f, + "CtG1({:?}, {:?}, {:?})", + self.0.x.limbs, self.0.y.limbs, self.0.z.limbs + ) + } +} + impl CtG1 { pub(crate) const fn from_xyz(x: bls12_381_fp, y: bls12_381_fp, z: bls12_381_fp) -> Self { CtG1(bls12_381_g1_jac { x, y, z }) @@ -94,8 +106,12 @@ impl G1 for CtG1 { let mut g1 = bls12_381_g1_jac::default(); unsafe { // The uncompress routine also checks that the point is on the curve - let res = constantine::ctt_bls12_381_deserialize_g1_compressed(&mut tmp, bytes.as_ptr()); - if res != ctt_codec_ecc_status::cttCodecEcc_Success && res != ctt_codec_ecc_status::cttCodecEcc_PointAtInfinity + let res = constantine::ctt_bls12_381_deserialize_g1_compressed( + &mut tmp, + bytes.as_ptr(), + ); + if res != ctt_codec_ecc_status::cttCodecEcc_Success + && res != ctt_codec_ecc_status::cttCodecEcc_PointAtInfinity { return Err("Failed to uncompress".to_string()); } @@ -113,7 +129,10 @@ impl G1 for CtG1 { fn to_bytes(&self) -> [u8; 48] { let mut out = [0u8; BYTES_PER_G1]; unsafe { - let _ = constantine::ctt_bls12_381_serialize_g1_compressed(out.as_mut_ptr(), &CtG1Affine::into_affine(&self).0); + let _ = constantine::ctt_bls12_381_serialize_g1_compressed( + out.as_mut_ptr(), + &CtG1Affine::into_affine(self).0, + ); } out } @@ -132,7 +151,8 @@ impl G1 for CtG1 { fn is_valid(&self) -> bool { unsafe { - constantine::ctt_bls12_381_validate_g1(&CtG1Affine::into_affine(&self).0) == ctt_codec_ecc_status::cttCodecEcc_Success + constantine::ctt_bls12_381_validate_g1(&CtG1Affine::into_affine(self).0) + == ctt_codec_ecc_status::cttCodecEcc_Success } } @@ -299,9 +319,21 @@ impl G1GetFp for CtG1 { } #[repr(C)] -#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)] +#[derive(Default, Clone, Copy)] pub struct CtG1Affine(pub constantine::bls12_381_g1_aff); +impl PartialEq for CtG1Affine { + fn eq(&self, other: &Self) -> bool { + unsafe { constantine::ctt_bls12_381_g1_aff_is_eq(&self.0, &other.0) != 0 } + } +} + +impl Debug for CtG1Affine { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + write!(f, "CtG1Affine({:?}, {:?})", self.0.x.limbs, self.0.y.limbs) + } +} + impl G1Affine for CtG1Affine { const ZERO: Self = Self(bls12_381_g1_aff { x: { diff --git a/constantine/src/types/g2.rs b/constantine/src/types/g2.rs index 10af6d6a9..1ee153f06 100644 --- a/constantine/src/types/g2.rs +++ b/constantine/src/types/g2.rs @@ -22,7 +22,7 @@ use constantine_sys::{ use constantine_sys as constantine; -#[derive(Debug, Default, Clone, Copy)] +#[derive(Default, Clone, Copy)] pub struct CtG2(pub bls12_381_g2_jac); impl CtG2 { @@ -138,8 +138,12 @@ impl G2 for CtG2 { let mut g2 = bls12_381_g2_jac::default(); unsafe { // The uncompress routine also checks that the point is on the curve - let res = constantine::ctt_bls12_381_deserialize_g2_compressed(&mut tmp, bytes.as_ptr()); - if res != ctt_codec_ecc_status::cttCodecEcc_Success && res != ctt_codec_ecc_status::cttCodecEcc_PointAtInfinity + let res = constantine::ctt_bls12_381_deserialize_g2_compressed( + &mut tmp, + bytes.as_ptr(), + ); + if res != ctt_codec_ecc_status::cttCodecEcc_Success + && res != ctt_codec_ecc_status::cttCodecEcc_PointAtInfinity { return Err("Failed to uncompress".to_string()); } @@ -154,7 +158,7 @@ impl G2 for CtG2 { let mut tmp = bls12_381_g2_aff::default(); unsafe { constantine::ctt_bls12_381_g2_jac_affine(&mut tmp, &self.0); - constantine::ctt_bls12_381_serialize_g2_compressed(out.as_mut_ptr(), &tmp); + let _ = constantine::ctt_bls12_381_serialize_g2_compressed(out.as_mut_ptr(), &tmp); } out } diff --git a/constantine/src/types/kzg_settings.rs b/constantine/src/types/kzg_settings.rs index 0ed954ab8..0f2ff600d 100644 --- a/constantine/src/types/kzg_settings.rs +++ b/constantine/src/types/kzg_settings.rs @@ -17,7 +17,7 @@ use crate::types::poly::CtPoly; use super::fp::CtFp; use super::g1::CtG1Affine; -#[derive(Debug, Clone, Default)] +#[derive(Clone, Default)] pub struct CtKZGSettings { pub fs: CtFFTSettings, pub secret_g1: Vec, diff --git a/kzg/src/lib.rs b/kzg/src/lib.rs index 5dccc3a10..bc546c19d 100644 --- a/kzg/src/lib.rs +++ b/kzg/src/lib.rs @@ -204,7 +204,7 @@ pub trait G1Fp: Clone + Default + Sync + Copy + PartialEq + Debug + Send { } pub trait G1Affine: - Clone + Default + PartialEq + Sync + Copy + Debug + Send + Clone + Default + PartialEq + Sync + Copy + Send + Debug { const ZERO: Self; From f997010aeb09bc6199f676e660e55eb4e283ad1f Mon Sep 17 00:00:00 2001 From: Domas Kalinauskas Date: Sun, 31 Dec 2023 13:43:03 +0200 Subject: [PATCH 08/17] switch constantine deps to repo --- Cargo.lock | 3 +++ constantine/Cargo.toml | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1b37936ef..dc2f658a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -397,6 +397,7 @@ checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" [[package]] name = "constantine-core" version = "0.1.0" +source = "git+https://github.com/lynxcs/constantine.git?branch=rust-kzg-changes#f9b4079cff3d7a83df89a613d4ef0ca18449d411" dependencies = [ "constantine-sys", ] @@ -404,6 +405,7 @@ dependencies = [ [[package]] name = "constantine-ethereum-kzg" version = "0.1.0" +source = "git+https://github.com/lynxcs/constantine.git?branch=rust-kzg-changes#f9b4079cff3d7a83df89a613d4ef0ca18449d411" dependencies = [ "constantine-core", "constantine-sys", @@ -412,6 +414,7 @@ dependencies = [ [[package]] name = "constantine-sys" version = "0.1.0" +source = "git+https://github.com/lynxcs/constantine.git?branch=rust-kzg-changes#f9b4079cff3d7a83df89a613d4ef0ca18449d411" [[package]] name = "cpufeatures" diff --git a/constantine/Cargo.toml b/constantine/Cargo.toml index 3d9057305..eaca6e5bf 100644 --- a/constantine/Cargo.toml +++ b/constantine/Cargo.toml @@ -8,9 +8,9 @@ blst = "0.3.11" kzg = { path = "../kzg", default-features = false } libc = { version = "0.2.148", default-features = false } once_cell = { version = "1.18.0", features = ["critical-section"], default-features = false } -constantine-ethereum-kzg = { path = "../../constantine/constantine-rust/constantine-ethereum-kzg" } -constantine-sys = { path = "../../constantine/constantine-rust/constantine-sys" } -constantine-core = { path = "../../constantine/constantine-rust/constantine-core" } +constantine-ethereum-kzg = { 'git' = 'https://github.com/lynxcs/constantine.git' , branch='rust-kzg-changes' } +constantine-sys = { 'git' = 'https://github.com/lynxcs/constantine.git' , branch='rust-kzg-changes' } +constantine-core = { 'git' = 'https://github.com/lynxcs/constantine.git' , branch='rust-kzg-changes' } rand = { version = "0.8.5", optional = true } rayon = { version = "1.8.0", optional = true } smallvec = { version = "1.11.1", features = ["const_generics"] } From 2824ac3c61dd0d8b0f4b662f1bfa8a61be76b412 Mon Sep 17 00:00:00 2001 From: Domas Kalinauskas Date: Sun, 31 Dec 2023 14:24:05 +0200 Subject: [PATCH 09/17] workflow setup nim --- .github/workflows/backend-benchmarks.yml | 14 ++++++++++++++ .github/workflows/backend-tests.yml | 15 +++++++++++++++ .github/workflows/release.yml | 14 ++++++++++++++ run-benchmarks.sh | 8 ++++++++ run-c-kzg-4844-benches.sh | 2 +- run-c-kzg-4844-tests.sh | 2 +- 6 files changed, 53 insertions(+), 2 deletions(-) diff --git a/.github/workflows/backend-benchmarks.yml b/.github/workflows/backend-benchmarks.yml index 6a459343c..f48e8b031 100644 --- a/.github/workflows/backend-benchmarks.yml +++ b/.github/workflows/backend-benchmarks.yml @@ -33,6 +33,20 @@ jobs: distribution: "temurin" java-version: "11" + - if: matrix.backend == 'constantine' + uses: jiro4989/setup-nim-action@v1 + with: + nim-version: '1.6.14' + + # Install constantine backend deps + - name: "constantine - install deps" + if: matrix.backend == 'constantine' && matrix.os == 'ubuntu-latest' + run: | + sudo DEBIAN_FRONTEND='noninteractive' apt-fast install \ + --no-install-recommends -yq \ + libgmp-dev \ + llvm + - uses: actions/setup-go@v2 with: go-version: ^1.19 diff --git a/.github/workflows/backend-tests.yml b/.github/workflows/backend-tests.yml index 4814e8744..6705e25ee 100644 --- a/.github/workflows/backend-tests.yml +++ b/.github/workflows/backend-tests.yml @@ -47,6 +47,12 @@ jobs: with: distribution: "temurin" java-version: "11" + + - if: matrix.backend == 'constantine' + uses: jiro4989/setup-nim-action@v1 + with: + nim-version: '1.6.14' + - uses: actions/setup-python@v4 with: python-version: '3.10' @@ -59,6 +65,15 @@ jobs: with: go-version: ^1.19 + # Install constantine backend deps + - name: "constantine - install deps" + if: matrix.backend == 'constantine' && matrix.os == 'ubuntu-latest' + run: | + sudo DEBIAN_FRONTEND='noninteractive' apt-fast install \ + --no-install-recommends -yq \ + libgmp-dev \ + llvm + # Check kzg clippy - name: "kzg clippy" if: matrix.exec_once_overall diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b10b14522..0fb4bd1b4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,6 +30,20 @@ jobs: steps: - uses: actions/checkout@v2 + - if: matrix.backend == 'constantine' + uses: jiro4989/setup-nim-action@v1 + with: + nim-version: '1.6.14' + + # Install constantine backend deps + - name: "constantine - install deps" + if: matrix.backend == 'constantine' && matrix.os == 'ubuntu-latest' + run: | + sudo DEBIAN_FRONTEND='noninteractive' apt-fast install \ + --no-install-recommends -yq \ + libgmp-dev \ + llvm + - if: matrix.target == 'windows' name: Install MinGW run: | diff --git a/run-benchmarks.sh b/run-benchmarks.sh index 5adbca9aa..792b21dcb 100644 --- a/run-benchmarks.sh +++ b/run-benchmarks.sh @@ -131,6 +131,14 @@ do print_msg "rust-kzg with mcl backend (parallel)" ../"$paste_file" taskset --cpu-list "${taskset_cpu_list[$i]}" cargo bench --manifest-path mcl/kzg-bench/Cargo.toml --features rust-kzg-mcl/parallel >> ../"$paste_file" + # 3.11. rust-kzg with constantine backend (sequential) + print_msg "rust-kzg with constantine backend (sequential)" ../"$paste_file" + taskset --cpu-list "${taskset_cpu_list[$i]}" cargo bench --manifest-path constantine/kzg-bench/Cargo.toml >> ../"$paste_file" + + # 3.12. rust-kzg with constantine backend (parallel) + print_msg "rust-kzg with constantine backend (parallel)" ../"$paste_file" + taskset --cpu-list "${taskset_cpu_list[$i]}" cargo bench --manifest-path constantine/kzg-bench/Cargo.toml --features rust-kzg-constantine/parallel >> ../"$paste_file" + # 3.11. rust binding (rust-kzg with blst backend) print_msg "rust binding (rust-kzg with blst backend)" ../"$paste_file" cd blst/c-kzg-4844/bindings/rust/ || exit diff --git a/run-c-kzg-4844-benches.sh b/run-c-kzg-4844-benches.sh index 6aacec196..6d38aea6f 100755 --- a/run-c-kzg-4844-benches.sh +++ b/run-c-kzg-4844-benches.sh @@ -16,7 +16,7 @@ while [[ -n $# ]]; do -p|--parallel) parallel=true ;; - blst|arkworks|mcl|zkcrypto) + blst|arkworks|mcl|zkcrypto|constantine) backend="$1" ;; *) diff --git a/run-c-kzg-4844-tests.sh b/run-c-kzg-4844-tests.sh index 9454dc32d..335c8a78d 100755 --- a/run-c-kzg-4844-tests.sh +++ b/run-c-kzg-4844-tests.sh @@ -16,7 +16,7 @@ while [[ -n $# ]]; do -p|--parallel) parallel=true ;; - blst|arkworks|mcl|zkcrypto) + blst|arkworks|mcl|zkcrypto|constantine) backend="$1" ;; *) From 1a0ef9765f36d829488346f37c5f4e76324123d3 Mon Sep 17 00:00:00 2001 From: Domas Kalinauskas Date: Wed, 3 Jan 2024 18:25:21 +0200 Subject: [PATCH 10/17] mixed: use compute_kzg_proof_parallel --- constantine/src/mixed_kzg/mixed_eip_4844.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/constantine/src/mixed_kzg/mixed_eip_4844.rs b/constantine/src/mixed_kzg/mixed_eip_4844.rs index f8758a933..88a775fe4 100644 --- a/constantine/src/mixed_kzg/mixed_eip_4844.rs +++ b/constantine/src/mixed_kzg/mixed_eip_4844.rs @@ -80,9 +80,17 @@ pub fn compute_kzg_proof_mixed( match s { MixedKzgSettings::Constantine(ctt_context) => { let blob_bytes = blob_fr_to_byte(blob)?; + + #[cfg(feature = "parallel")] + let res = ctt_context + .ctx + .compute_kzg_proof_parallel(&ctt_context.pool, &blob_bytes, &z.to_bytes()); + + #[cfg(not(feature = "parallel"))] let res = ctt_context .ctx .compute_kzg_proof(&blob_bytes, &z.to_bytes()); + match res { Ok((proof, y)) => Ok((CtG1::from_bytes(&proof)?, CtFr::from_bytes(&y)?)), Err(x) => Err(x.to_string()), From 30b1aa6faec5c8a770970474e13621556a231a9b Mon Sep 17 00:00:00 2001 From: Armantidas Date: Sat, 6 Jan 2024 18:34:32 +0200 Subject: [PATCH 11/17] Constantine - use batch affine --- Cargo.lock | 6 +++--- constantine/src/types/g1.rs | 20 +++----------------- 2 files changed, 6 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dc2f658a1..631ccee7a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -397,7 +397,7 @@ checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" [[package]] name = "constantine-core" version = "0.1.0" -source = "git+https://github.com/lynxcs/constantine.git?branch=rust-kzg-changes#f9b4079cff3d7a83df89a613d4ef0ca18449d411" +source = "git+https://github.com/lynxcs/constantine.git?branch=rust-kzg-changes#d00c6f9b1ed1c3e0d8b68340d05bd861aa69cd35" dependencies = [ "constantine-sys", ] @@ -405,7 +405,7 @@ dependencies = [ [[package]] name = "constantine-ethereum-kzg" version = "0.1.0" -source = "git+https://github.com/lynxcs/constantine.git?branch=rust-kzg-changes#f9b4079cff3d7a83df89a613d4ef0ca18449d411" +source = "git+https://github.com/lynxcs/constantine.git?branch=rust-kzg-changes#d00c6f9b1ed1c3e0d8b68340d05bd861aa69cd35" dependencies = [ "constantine-core", "constantine-sys", @@ -414,7 +414,7 @@ dependencies = [ [[package]] name = "constantine-sys" version = "0.1.0" -source = "git+https://github.com/lynxcs/constantine.git?branch=rust-kzg-changes#f9b4079cff3d7a83df89a613d4ef0ca18449d411" +source = "git+https://github.com/lynxcs/constantine.git?branch=rust-kzg-changes#d00c6f9b1ed1c3e0d8b68340d05bd861aa69cd35" [[package]] name = "cpufeatures" diff --git a/constantine/src/types/g1.rs b/constantine/src/types/g1.rs index 13969a332..99c6ce67b 100644 --- a/constantine/src/types/g1.rs +++ b/constantine/src/types/g1.rs @@ -357,23 +357,9 @@ impl G1Affine for CtG1Affine { } fn into_affines_loc(out: &mut [Self], g1: &[CtG1]) { - g1.iter() - .zip(out.iter_mut()) - .for_each(|(g, out_slot)| unsafe { - constantine::ctt_bls12_381_g1_jac_affine(&mut out_slot.0, &g.0); - }); - } - - fn into_affines(g1: &[CtG1]) -> Vec { - g1.iter() - .map(|g| { - let mut ret = Self::default(); - unsafe { - constantine::ctt_bls12_381_g1_jac_affine(&mut ret.0, &g.0); - } - ret - }) - .collect::>() + unsafe{ + constantine::ctt_bls12_381_g1_jac_batch_affine(core::mem::transmute(out.as_mut_ptr()), core::mem::transmute(g1.as_ptr()), g1.len()); + } } fn to_proj(&self) -> CtG1 { From 247e0f94d15b06005d48c12492eb89d33f4e4b03 Mon Sep 17 00:00:00 2001 From: Domas Kalinauskas Date: Sun, 7 Jan 2024 20:07:43 +0200 Subject: [PATCH 12/17] switch to constantine-public-sys branch --- Cargo.lock | 6 +- constantine/Cargo.toml | 6 +- constantine/src/mixed_kzg/mixed_eip_4844.rs | 8 +- constantine/src/types/fp.rs | 6 +- constantine/src/types/fr.rs | 12 +- constantine/src/types/g1.rs | 42 +++++-- constantine/src/types/g2.rs | 130 ++++++++++---------- 7 files changed, 124 insertions(+), 86 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 631ccee7a..5efd23564 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -397,7 +397,7 @@ checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" [[package]] name = "constantine-core" version = "0.1.0" -source = "git+https://github.com/lynxcs/constantine.git?branch=rust-kzg-changes#d00c6f9b1ed1c3e0d8b68340d05bd861aa69cd35" +source = "git+https://github.com/mratsim/constantine.git?branch=constantine-public-sys#6bdb0897ff5f22d4a4f05425b426e85336eb0712" dependencies = [ "constantine-sys", ] @@ -405,7 +405,7 @@ dependencies = [ [[package]] name = "constantine-ethereum-kzg" version = "0.1.0" -source = "git+https://github.com/lynxcs/constantine.git?branch=rust-kzg-changes#d00c6f9b1ed1c3e0d8b68340d05bd861aa69cd35" +source = "git+https://github.com/mratsim/constantine.git?branch=constantine-public-sys#6bdb0897ff5f22d4a4f05425b426e85336eb0712" dependencies = [ "constantine-core", "constantine-sys", @@ -414,7 +414,7 @@ dependencies = [ [[package]] name = "constantine-sys" version = "0.1.0" -source = "git+https://github.com/lynxcs/constantine.git?branch=rust-kzg-changes#d00c6f9b1ed1c3e0d8b68340d05bd861aa69cd35" +source = "git+https://github.com/mratsim/constantine.git?branch=constantine-public-sys#6bdb0897ff5f22d4a4f05425b426e85336eb0712" [[package]] name = "cpufeatures" diff --git a/constantine/Cargo.toml b/constantine/Cargo.toml index eaca6e5bf..6078fdaf8 100644 --- a/constantine/Cargo.toml +++ b/constantine/Cargo.toml @@ -8,9 +8,9 @@ blst = "0.3.11" kzg = { path = "../kzg", default-features = false } libc = { version = "0.2.148", default-features = false } once_cell = { version = "1.18.0", features = ["critical-section"], default-features = false } -constantine-ethereum-kzg = { 'git' = 'https://github.com/lynxcs/constantine.git' , branch='rust-kzg-changes' } -constantine-sys = { 'git' = 'https://github.com/lynxcs/constantine.git' , branch='rust-kzg-changes' } -constantine-core = { 'git' = 'https://github.com/lynxcs/constantine.git' , branch='rust-kzg-changes' } +constantine-ethereum-kzg = { 'git' = 'https://github.com/mratsim/constantine.git' , branch='constantine-public-sys' } +constantine-sys = { 'git' = 'https://github.com/mratsim/constantine.git' , branch='constantine-public-sys' } +constantine-core = { 'git' = 'https://github.com/mratsim/constantine.git' , branch='constantine-public-sys' } rand = { version = "0.8.5", optional = true } rayon = { version = "1.8.0", optional = true } smallvec = { version = "1.11.1", features = ["const_generics"] } diff --git a/constantine/src/mixed_kzg/mixed_eip_4844.rs b/constantine/src/mixed_kzg/mixed_eip_4844.rs index 88a775fe4..db331f162 100644 --- a/constantine/src/mixed_kzg/mixed_eip_4844.rs +++ b/constantine/src/mixed_kzg/mixed_eip_4844.rs @@ -82,9 +82,11 @@ pub fn compute_kzg_proof_mixed( let blob_bytes = blob_fr_to_byte(blob)?; #[cfg(feature = "parallel")] - let res = ctt_context - .ctx - .compute_kzg_proof_parallel(&ctt_context.pool, &blob_bytes, &z.to_bytes()); + let res = ctt_context.ctx.compute_kzg_proof_parallel( + &ctt_context.pool, + &blob_bytes, + &z.to_bytes(), + ); #[cfg(not(feature = "parallel"))] let res = ctt_context diff --git a/constantine/src/types/fp.rs b/constantine/src/types/fp.rs index adbc36191..ac66b3acf 100644 --- a/constantine/src/types/fp.rs +++ b/constantine/src/types/fp.rs @@ -69,7 +69,11 @@ impl G1Fp for CtFp { } fn from_underlying_arr(arr: &[u64; 6]) -> Self { - Self(bls12_381_fp { limbs: *arr }) + unsafe { + Self(bls12_381_fp { + limbs: core::mem::transmute(*arr), + }) + } } fn neg_assign(&mut self) { diff --git a/constantine/src/types/fr.rs b/constantine/src/types/fr.rs index bdb696ddd..a2f43eac3 100644 --- a/constantine/src/types/fr.rs +++ b/constantine/src/types/fr.rs @@ -38,11 +38,19 @@ impl Eq for CtFr {} impl CtFr { pub fn from_blst_fr(fr: blst::blst_fr) -> Self { - Self(bls12_381_fr { limbs: fr.l }) + unsafe { + Self(bls12_381_fr { + limbs: core::mem::transmute(fr.l), + }) + } } pub fn to_blst_fr(&self) -> blst_fr { - blst_fr { l: self.0.limbs } + unsafe { + blst_fr { + l: core::mem::transmute(self.0.limbs), + } + } } } diff --git a/constantine/src/types/g1.rs b/constantine/src/types/g1.rs index 99c6ce67b..8f76f7bdd 100644 --- a/constantine/src/types/g1.rs +++ b/constantine/src/types/g1.rs @@ -56,18 +56,34 @@ impl CtG1 { } pub const fn from_blst_p1(p1: blst::blst_p1) -> Self { - Self(bls12_381_g1_jac { - x: bls12_381_fp { limbs: p1.x.l }, - y: bls12_381_fp { limbs: p1.y.l }, - z: bls12_381_fp { limbs: p1.z.l }, - }) + unsafe { + Self(bls12_381_g1_jac { + x: bls12_381_fp { + limbs: core::mem::transmute(p1.x.l), + }, + y: bls12_381_fp { + limbs: core::mem::transmute(p1.y.l), + }, + z: bls12_381_fp { + limbs: core::mem::transmute(p1.z.l), + }, + }) + } } pub const fn to_blst_p1(&self) -> blst::blst_p1 { - blst::blst_p1 { - x: blst::blst_fp { l: self.0.x.limbs }, - y: blst::blst_fp { l: self.0.y.limbs }, - z: blst::blst_fp { l: self.0.z.limbs }, + unsafe { + blst::blst_p1 { + x: blst::blst_fp { + l: core::mem::transmute(self.0.x.limbs), + }, + y: blst::blst_fp { + l: core::mem::transmute(self.0.y.limbs), + }, + z: blst::blst_fp { + l: core::mem::transmute(self.0.z.limbs), + }, + } } } } @@ -357,8 +373,12 @@ impl G1Affine for CtG1Affine { } fn into_affines_loc(out: &mut [Self], g1: &[CtG1]) { - unsafe{ - constantine::ctt_bls12_381_g1_jac_batch_affine(core::mem::transmute(out.as_mut_ptr()), core::mem::transmute(g1.as_ptr()), g1.len()); + unsafe { + constantine::ctt_bls12_381_g1_jac_batch_affine( + core::mem::transmute(out.as_mut_ptr()), + core::mem::transmute(g1.as_ptr()), + g1.len(), + ); } } diff --git a/constantine/src/types/g2.rs b/constantine/src/types/g2.rs index 1ee153f06..fa50da02b 100644 --- a/constantine/src/types/g2.rs +++ b/constantine/src/types/g2.rs @@ -27,72 +27,76 @@ pub struct CtG2(pub bls12_381_g2_jac); impl CtG2 { pub const fn from_blst_p2(p2: blst::blst_p2) -> Self { - Self(bls12_381_g2_jac { - x: bls12_381_fp2 { - c: [ - bls12_381_fp { - limbs: p2.x.fp[0].l, - }, - bls12_381_fp { - limbs: p2.x.fp[1].l, - }, - ], - }, - y: bls12_381_fp2 { - c: [ - bls12_381_fp { - limbs: p2.y.fp[0].l, - }, - bls12_381_fp { - limbs: p2.y.fp[1].l, - }, - ], - }, - z: bls12_381_fp2 { - c: [ - bls12_381_fp { - limbs: p2.z.fp[0].l, - }, - bls12_381_fp { - limbs: p2.z.fp[1].l, - }, - ], - }, - }) + unsafe { + Self(bls12_381_g2_jac { + x: bls12_381_fp2 { + c: [ + bls12_381_fp { + limbs: core::mem::transmute(p2.x.fp[0].l), + }, + bls12_381_fp { + limbs: core::mem::transmute(p2.x.fp[1].l), + }, + ], + }, + y: bls12_381_fp2 { + c: [ + bls12_381_fp { + limbs: core::mem::transmute(p2.y.fp[0].l), + }, + bls12_381_fp { + limbs: core::mem::transmute(p2.y.fp[1].l), + }, + ], + }, + z: bls12_381_fp2 { + c: [ + bls12_381_fp { + limbs: core::mem::transmute(p2.z.fp[0].l), + }, + bls12_381_fp { + limbs: core::mem::transmute(p2.z.fp[1].l), + }, + ], + }, + }) + } } pub const fn to_blst_p2(&self) -> blst::blst_p2 { - blst::blst_p2 { - x: blst::blst_fp2 { - fp: [ - blst::blst_fp { - l: self.0.x.c[0].limbs, - }, - blst::blst_fp { - l: self.0.x.c[1].limbs, - }, - ], - }, - y: blst::blst_fp2 { - fp: [ - blst::blst_fp { - l: self.0.y.c[0].limbs, - }, - blst::blst_fp { - l: self.0.y.c[1].limbs, - }, - ], - }, - z: blst::blst_fp2 { - fp: [ - blst::blst_fp { - l: self.0.z.c[0].limbs, - }, - blst::blst_fp { - l: self.0.z.c[1].limbs, - }, - ], - }, + unsafe { + blst::blst_p2 { + x: blst::blst_fp2 { + fp: [ + blst::blst_fp { + l: core::mem::transmute(self.0.x.c[0].limbs), + }, + blst::blst_fp { + l: core::mem::transmute(self.0.x.c[1].limbs), + }, + ], + }, + y: blst::blst_fp2 { + fp: [ + blst::blst_fp { + l: core::mem::transmute(self.0.y.c[0].limbs), + }, + blst::blst_fp { + l: core::mem::transmute(self.0.y.c[1].limbs), + }, + ], + }, + z: blst::blst_fp2 { + fp: [ + blst::blst_fp { + l: core::mem::transmute(self.0.z.c[0].limbs), + }, + blst::blst_fp { + l: core::mem::transmute(self.0.z.c[1].limbs), + }, + ], + }, + } } } } From 595b20a675ba1488c2256fae9520bbebe021f7b7 Mon Sep 17 00:00:00 2001 From: Domas Kalinauskas Date: Thu, 11 Jan 2024 15:45:07 +0200 Subject: [PATCH 13/17] update constantine ver --- Cargo.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5efd23564..f8532c301 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -397,7 +397,7 @@ checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" [[package]] name = "constantine-core" version = "0.1.0" -source = "git+https://github.com/mratsim/constantine.git?branch=constantine-public-sys#6bdb0897ff5f22d4a4f05425b426e85336eb0712" +source = "git+https://github.com/mratsim/constantine.git?branch=constantine-public-sys#9f85b8395456e726afc2633f8d8d1ed5e5877a3e" dependencies = [ "constantine-sys", ] @@ -405,7 +405,7 @@ dependencies = [ [[package]] name = "constantine-ethereum-kzg" version = "0.1.0" -source = "git+https://github.com/mratsim/constantine.git?branch=constantine-public-sys#6bdb0897ff5f22d4a4f05425b426e85336eb0712" +source = "git+https://github.com/mratsim/constantine.git?branch=constantine-public-sys#9f85b8395456e726afc2633f8d8d1ed5e5877a3e" dependencies = [ "constantine-core", "constantine-sys", @@ -414,7 +414,7 @@ dependencies = [ [[package]] name = "constantine-sys" version = "0.1.0" -source = "git+https://github.com/mratsim/constantine.git?branch=constantine-public-sys#6bdb0897ff5f22d4a4f05425b426e85336eb0712" +source = "git+https://github.com/mratsim/constantine.git?branch=constantine-public-sys#9f85b8395456e726afc2633f8d8d1ed5e5877a3e" [[package]] name = "cpufeatures" From 5ec4bbddeb8234e8875a8c4bbf1f81c9172eac22 Mon Sep 17 00:00:00 2001 From: Domas Kalinauskas Date: Thu, 11 Jan 2024 15:48:32 +0200 Subject: [PATCH 14/17] constantine g1,g2 mul remove blst usage --- constantine/src/types/g1.rs | 28 ++-------------------------- constantine/src/types/g2.rs | 13 +++---------- 2 files changed, 5 insertions(+), 36 deletions(-) diff --git a/constantine/src/types/g1.rs b/constantine/src/types/g1.rs index 8f76f7bdd..bb157038e 100644 --- a/constantine/src/types/g1.rs +++ b/constantine/src/types/g1.rs @@ -245,33 +245,9 @@ impl G1 for CtG1 { impl G1Mul for CtG1 { fn mul(&self, b: &CtFr) -> Self { - // FIXME: No transmute here, use constantine - let mut scalar = blst::blst_scalar::default(); + let mut result = *self; unsafe { - blst::blst_scalar_from_fr(&mut scalar, ptr_transmute(&b.0)); - } - - // Count the number of bytes to be multiplied. - let mut i = scalar.b.len(); - while i != 0 && scalar.b[i - 1] == 0 { - i -= 1; - } - - let mut result = Self::default(); - if i == 0 { - return G1_IDENTITY; - } else if i == 1 && scalar.b[0] == 1 { - return *self; - } else { - // Count the number of bits to be multiplied. - unsafe { - blst::blst_p1_mult( - ptr_transmute_mut(&mut result.0), - ptr_transmute(&self.0), - &(scalar.b[0]), - 8 * i - 7 + log_2_byte(scalar.b[i - 1]), - ); - } + constantine::ctt_bls12_381_g1_jac_scalar_mul_fr_coef(&mut result.0, &b.0); } result } diff --git a/constantine/src/types/g2.rs b/constantine/src/types/g2.rs index fa50da02b..a15a7bc90 100644 --- a/constantine/src/types/g2.rs +++ b/constantine/src/types/g2.rs @@ -103,18 +103,11 @@ impl CtG2 { impl G2Mul for CtG2 { fn mul(&self, b: &CtFr) -> Self { - let mut result = bls12_381_g2_jac::default(); - let mut scalar = blst::blst_scalar::default(); + let mut result = *self; unsafe { - blst::blst_scalar_from_fr(&mut scalar, ptr_transmute(&b.0)); - blst::blst_p2_mult( - ptr_transmute_mut(&mut result), - ptr_transmute(&self.0), - scalar.b.as_ptr(), - 8 * core::mem::size_of::(), - ); + constantine::ctt_bls12_381_g2_jac_scalar_mul_fr_coef(&mut result.0, &b.0); } - Self(result) + result } } From dd32072bdb1140cc7c2d16e34f8c0db036a91c60 Mon Sep 17 00:00:00 2001 From: Armantidas Date: Thu, 11 Jan 2024 16:07:17 +0200 Subject: [PATCH 15/17] CtFr remove blst usage --- constantine/src/types/fr.rs | 46 +++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/constantine/src/types/fr.rs b/constantine/src/types/fr.rs index a2f43eac3..ca9e67481 100644 --- a/constantine/src/types/fr.rs +++ b/constantine/src/types/fr.rs @@ -68,16 +68,17 @@ impl Fr for CtFr { } #[cfg(feature = "rand")] - fn rand() -> Self { - let val: [u64; 4] = [ - rand::random(), - rand::random(), - rand::random(), - rand::random(), - ]; + fn rand() -> Self { + let val = + constantine_sys::big255 { limbs:[ + rand::random(), + rand::random(), + rand::random(), + rand::random(), + ]}; let mut ret = Self::default(); unsafe { - blst::blst_fr_from_uint64(ptr_transmute_mut(&mut ret.0), val.as_ptr()); + constantine::ctt_bls12_381_fr_from_big255(&mut ret.0, &val); } ret @@ -95,18 +96,17 @@ impl Fr for CtFr { }) .and_then(|bytes: &[u8; BYTES_PER_FIELD_ELEMENT]| { let mut ret: Self = Self::default(); - let mut bls_scalar = blst::blst_scalar::default(); + let mut scalar = constantine::big255::default(); unsafe { let status = constantine::ctt_bls12_381_deserialize_scalar( - ptr_transmute_mut(&mut bls_scalar), + &mut scalar, bytes.as_ptr(), ); if status == ctt_codec_scalar_status::cttCodecScalar_ScalarLargerThanCurveOrder { return Err("Invalid scalar".to_string()); } - // FIXME: Change when big255->Fr conversion is available - blst::blst_fr_from_scalar(ptr_transmute_mut(&mut ret.0), &bls_scalar); + constantine::ctt_bls12_381_fr_from_big255(&mut ret.0, &scalar); } Ok(ret) }) @@ -124,11 +124,11 @@ impl Fr for CtFr { }) .map(|bytes: &[u8; BYTES_PER_FIELD_ELEMENT]| { let mut ret = Self::default(); - let mut bls_scalar = blst::blst_scalar::default(); + let mut scalar = constantine::big255::default(); unsafe { // FIXME: Seems like no 'non-validating' variant exists in constantine - blst::blst_scalar_from_bendian(&mut bls_scalar, bytes.as_ptr()); - blst::blst_fr_from_scalar(ptr_transmute_mut(&mut ret.0), &bls_scalar); + blst::blst_scalar_from_bendian(ptr_transmute_mut(&mut scalar), bytes.as_ptr()); + constantine::ctt_bls12_381_fr_from_big255(&mut ret.0, &scalar); } ret }) @@ -142,8 +142,7 @@ impl Fr for CtFr { fn from_u64_arr(u: &[u64; 4]) -> Self { let mut ret = Self::default(); unsafe { - // FIXME: Change when big255->Fr conversion is available - blst::blst_fr_from_uint64(ptr_transmute_mut(&mut ret.0), u.as_ptr()); + constantine::ctt_bls12_381_fr_from_big255(&mut ret.0, ptr_transmute(u)); } ret @@ -157,8 +156,7 @@ impl Fr for CtFr { let mut scalar = constantine::big255::default(); let mut bytes = [0u8; 32]; unsafe { - // FIXME: Change when Fr->Big255 conversion is available - blst::blst_scalar_from_fr(ptr_transmute_mut(&mut scalar), ptr_transmute(&self.0)); + constantine::ctt_big255_from_bls12_381_fr(&mut scalar, &self.0); let _ = constantine::ctt_bls12_381_serialize_scalar(bytes.as_mut_ptr(), &scalar); } @@ -168,8 +166,7 @@ impl Fr for CtFr { fn to_u64_arr(&self) -> [u64; 4] { let mut val: [u64; 4] = [0; 4]; unsafe { - // FIXME: Change when Fr->Big255 conversion is available - blst::blst_uint64_from_fr(val.as_mut_ptr(), ptr_transmute(&self.0)); + constantine::ctt_big255_from_bls12_381_fr(ptr_transmute_mut(&mut val), &self.0); } val @@ -280,11 +277,10 @@ impl Fr for CtFr { } fn to_scalar(&self) -> kzg::Scalar256 { - // FIXME: Change to constantine version when available - let mut blst_scalar = blst::blst_scalar::default(); + let mut scalar = constantine::big255::default(); unsafe { - blst::blst_scalar_from_fr(&mut blst_scalar, ptr_transmute(&self.0)); + constantine::ctt_big255_from_bls12_381_fr(&mut scalar, &self.0); + Scalar256::from_u64(core::mem::transmute(scalar.limbs)) } - Scalar256::from_u8(&blst_scalar.b) } } From d08d3f93c2bae1d56baec9ee009e9ad8b6195f3a Mon Sep 17 00:00:00 2001 From: Domas Kalinauskas Date: Thu, 11 Jan 2024 16:08:40 +0200 Subject: [PATCH 16/17] fmt & clippy fixes --- constantine/src/types/fr.rs | 17 ++++++++--------- constantine/src/types/g1.rs | 4 +--- constantine/src/types/g2.rs | 2 -- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/constantine/src/types/fr.rs b/constantine/src/types/fr.rs index ca9e67481..5880938c7 100644 --- a/constantine/src/types/fr.rs +++ b/constantine/src/types/fr.rs @@ -68,14 +68,15 @@ impl Fr for CtFr { } #[cfg(feature = "rand")] - fn rand() -> Self { - let val = - constantine_sys::big255 { limbs:[ + fn rand() -> Self { + let val = constantine_sys::big255 { + limbs: [ rand::random(), rand::random(), rand::random(), - rand::random(), - ]}; + rand::random(), + ], + }; let mut ret = Self::default(); unsafe { constantine::ctt_bls12_381_fr_from_big255(&mut ret.0, &val); @@ -98,10 +99,8 @@ impl Fr for CtFr { let mut ret: Self = Self::default(); let mut scalar = constantine::big255::default(); unsafe { - let status = constantine::ctt_bls12_381_deserialize_scalar( - &mut scalar, - bytes.as_ptr(), - ); + let status = + constantine::ctt_bls12_381_deserialize_scalar(&mut scalar, bytes.as_ptr()); if status == ctt_codec_scalar_status::cttCodecScalar_ScalarLargerThanCurveOrder { return Err("Invalid scalar".to_string()); diff --git a/constantine/src/types/g1.rs b/constantine/src/types/g1.rs index bb157038e..c9fdae176 100644 --- a/constantine/src/types/g1.rs +++ b/constantine/src/types/g1.rs @@ -12,9 +12,7 @@ use core::fmt::{Debug, Formatter}; use crate::kzg_proofs::g1_linear_combination; use crate::types::fp::CtFp; use crate::types::fr::CtFr; -use crate::utils::ptr_transmute; -use crate::utils::ptr_transmute_mut; -use kzg::common_utils::log_2_byte; + use kzg::eip_4844::BYTES_PER_G1; use kzg::G1Affine; use kzg::G1GetFp; diff --git a/constantine/src/types/g2.rs b/constantine/src/types/g2.rs index a15a7bc90..f7ae5dea5 100644 --- a/constantine/src/types/g2.rs +++ b/constantine/src/types/g2.rs @@ -12,8 +12,6 @@ use kzg::{G2Mul, G2}; use crate::consts::{G2_GENERATOR, G2_NEGATIVE_GENERATOR}; use crate::types::fr::CtFr; -use crate::utils::ptr_transmute; -use crate::utils::ptr_transmute_mut; use constantine_sys::{ bls12_381_fp, bls12_381_fp2, bls12_381_g2_aff, bls12_381_g2_jac, From e6319538e299a2570634b6d2cbc2be4de8b36981 Mon Sep 17 00:00:00 2001 From: Domas Kalinauskas Date: Thu, 11 Jan 2024 19:34:06 +0200 Subject: [PATCH 17/17] constantine - remove usage of blst in from_bytes_unchecked --- constantine/src/types/fr.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/constantine/src/types/fr.rs b/constantine/src/types/fr.rs index 5880938c7..2d429defd 100644 --- a/constantine/src/types/fr.rs +++ b/constantine/src/types/fr.rs @@ -125,8 +125,11 @@ impl Fr for CtFr { let mut ret = Self::default(); let mut scalar = constantine::big255::default(); unsafe { - // FIXME: Seems like no 'non-validating' variant exists in constantine - blst::blst_scalar_from_bendian(ptr_transmute_mut(&mut scalar), bytes.as_ptr()); + let _ = constantine::ctt_big255_unmarshalBE( + &mut scalar, + bytes.as_ptr(), + BYTES_PER_FIELD_ELEMENT as isize, + ); constantine::ctt_bls12_381_fr_from_big255(&mut ret.0, &scalar); } ret

(header_path: &str, blst_headers_dir: &str, bindings_out_path: P) ++where + P: AsRef, + { + use bindgen::Builder; +-- +2.34.1 + diff --git a/constantine/src/consts.rs b/constantine/src/consts.rs new file mode 100644 index 000000000..67b22320e --- /dev/null +++ b/constantine/src/consts.rs @@ -0,0 +1,214 @@ +use crate::kzg_types::{ZG1, ZG2}; +use bls12_381::{Fp as ZFp, Fp2 as ZFp2, G1Projective, G2Projective}; +pub const SCALE_FACTOR: u64 = 5; +pub const NUM_ROOTS: usize = 32; +#[rustfmt::skip] +pub const SCALE2_ROOT_OF_UNITY: [[u64; 4]; 32] = [ + [0x0000000000000001, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000], + [0xffffffff00000000, 0x53bda402fffe5bfe, 0x3339d80809a1d805, 0x73eda753299d7d48], + [0x0001000000000000, 0xec03000276030000, 0x8d51ccce760304d0, 0x0000000000000000], + [0x7228fd3397743f7a, 0xb38b21c28713b700, 0x8c0625cd70d77ce2, 0x345766f603fa66e7], + [0x53ea61d87742bcce, 0x17beb312f20b6f76, 0xdd1c0af834cec32c, 0x20b1ce9140267af9], + [0x360c60997369df4e, 0xbf6e88fb4c38fb8a, 0xb4bcd40e22f55448, 0x50e0903a157988ba], + [0x8140d032f0a9ee53, 0x2d967f4be2f95155, 0x14a1e27164d8fdbd, 0x45af6345ec055e4d], + [0x5130c2c1660125be, 0x98d0caac87f5713c, 0xb7c68b4d7fdd60d0, 0x6898111413588742], + [0x4935bd2f817f694b, 0x0a0865a899e8deff, 0x6b368121ac0cf4ad, 0x4f9b4098e2e9f12e], + [0x4541b8ff2ee0434e, 0xd697168a3a6000fe, 0x39feec240d80689f, 0x095166525526a654], + [0x3c28d666a5c2d854, 0xea437f9626fc085e, 0x8f4de02c0f776af3, 0x325db5c3debf77a1], + [0x4a838b5d59cd79e5, 0x55ea6811be9c622d, 0x09f1ca610a08f166, 0x6d031f1b5c49c834], + [0xe206da11a5d36306, 0x0ad1347b378fbf96, 0xfc3e8acfe0f8245f, 0x564c0a11a0f704f4], + [0x6fdd00bfc78c8967, 0x146b58bc434906ac, 0x2ccddea2972e89ed, 0x485d512737b1da3d], + [0x034d2ff22a5ad9e1, 0xae4622f6a9152435, 0xdc86b01c0d477fa6, 0x56624634b500a166], + [0xfbd047e11279bb6e, 0xc8d5f51db3f32699, 0x483405417a0cbe39, 0x3291357ee558b50d], + [0xd7118f85cd96b8ad, 0x67a665ae1fcadc91, 0x88f39a78f1aeb578, 0x2155379d12180caa], + [0x08692405f3b70f10, 0xcd7f2bd6d0711b7d, 0x473a2eef772c33d6, 0x224262332d8acbf4], + [0x6f421a7d8ef674fb, 0xbb97a3bf30ce40fd, 0x652f717ae1c34bb0, 0x2d3056a530794f01], + [0x194e8c62ecb38d9d, 0xad8e16e84419c750, 0xdf625e80d0adef90, 0x520e587a724a6955], + [0xfece7e0e39898d4b, 0x2f69e02d265e09d9, 0xa57a6e07cb98de4a, 0x03e1c54bcb947035], + [0xcd3979122d3ea03a, 0x46b3105f04db5844, 0xc70d0874b0691d4e, 0x47c8b5817018af4f], + [0xc6e7a6ffb08e3363, 0xe08fec7c86389bee, 0xf2d38f10fbb8d1bb, 0x0abe6a5e5abcaa32], + [0x5616c57de0ec9eae, 0xc631ffb2585a72db, 0x5121af06a3b51e3c, 0x73560252aa0655b2], + [0x92cf4deb77bd779c, 0x72cf6a8029b7d7bc, 0x6e0bcd91ee762730, 0x291cf6d68823e687], + [0xce32ef844e11a51e, 0xc0ba12bb3da64ca5, 0x0454dc1edc61a1a3, 0x019fe632fd328739], + [0x531a11a0d2d75182, 0x02c8118402867ddc, 0x116168bffbedc11d, 0x0a0a77a3b1980c0d], + [0xe2d0a7869f0319ed, 0xb94f1101b1d7a628, 0xece8ea224f31d25d, 0x23397a9300f8f98b], + [0xd7b688830a4f2089, 0x6558e9e3f6ac7b41, 0x99e276b571905a7d, 0x52dd465e2f094256], + [0x474650359d8e211b, 0x84d37b826214abc6, 0x8da40c1ef2bb4598, 0x0c83ea7744bf1bee], + [0x694341f608c9dd56, 0xed3a181fabb30adc, 0x1339a815da8b398f, 0x2c6d4e4511657e1e], + [0x63e7cb4906ffc93f, 0xf070bb00e28a193d, 0xad1715b02e5713b5, 0x4b5371495990693f], +]; + +/** The G1 generator */ +pub const G1_GENERATOR: ZG1 = ZG1::from_g1_projective(G1Projective { + x: ZFp::from_raw_unchecked([ + 0x5cb3_8790_fd53_0c16, + 0x7817_fc67_9976_fff5, + 0x154f_95c7_143b_a1c1, + 0xf0ae_6acd_f3d0_e747, + 0xedce_6ecc_21db_f440, + 0x1201_7741_9e0b_fb75, + ]), + y: ZFp::from_raw_unchecked([ + 0xbaac_93d5_0ce7_2271, + 0x8c22_631a_7918_fd8e, + 0xdd59_5f13_5707_25ce, + 0x51ac_5829_5040_5194, + 0x0e1c_8c3f_ad00_59c0, + 0x0bbc_3efc_5008_a26a, + ]), + z: ZFp::from_raw_unchecked([ + 0x7609_0000_0002_fffd, + 0xebf4_000b_c40c_0002, + 0x5f48_9857_53c7_58ba, + 0x77ce_5853_7052_5745, + 0x5c07_1a97_a256_ec6d, + 0x15f6_5ec3_fa80_e493, + ]), +}); + +pub const G1_NEGATIVE_GENERATOR: ZG1 = ZG1::from_g1_projective(G1Projective { + x: ZFp::from_raw_unchecked([ + 0x5cb3_8790_fd53_0c16, + 0x7817_fc67_9976_fff5, + 0x154f_95c7_143b_a1c1, + 0xf0ae_6acd_f3d0_e747, + 0xedce_6ecc_21db_f440, + 0x1201_7741_9e0b_fb75, + ]), + y: ZFp::from_raw_unchecked([ + 0xff52_6c2a_f318_883a, + 0x9289_9ce4_383b_0270, + 0x89d7_738d_9fa9_d055, + 0x12ca_f35b_a344_c12a, + 0x3cff_1b76_964b_5317, + 0x0e44_d2ed_e977_4430, + ]), + z: ZFp::from_raw_unchecked([ + 0x7609_0000_0002_fffd, + 0xebf4_000b_c40c_0002, + 0x5f48_9857_53c7_58ba, + 0x77ce_5853_7052_5745, + 0x5c07_1a97_a256_ec6d, + 0x15f6_5ec3_fa80_e493, + ]), +}); +#[rustfmt::skip] +pub const G1_IDENTITY: ZG1 = ZG1::from_g1_projective( G1Projective { + x: ZFp::zero(), + y: ZFp::one(), + z: ZFp::zero(), +}); + +pub const G2_GENERATOR: ZG2 = ZG2::from_g2_projective(G2Projective { + x: ZFp2 { + c0: ZFp([ + 0xf5f28fa202940a10, + 0xb3f5fb2687b4961a, + 0xa1a893b53e2ae580, + 0x9894999d1a3caee9, + 0x6f67b7631863366b, + 0x058191924350bcd7, + ]), + c1: ZFp([ + 0xa5a9c0759e23f606, + 0xaaa0c59dbccd60c3, + 0x3bb17e18e2867806, + 0x1b1ab6cc8541b367, + 0xc2b6ed0ef2158547, + 0x11922a097360edf3, + ]), + }, + y: ZFp2 { + c0: ZFp([ + 0x4c730af860494c4a, + 0x597cfa1f5e369c5a, + 0xe7e6856caa0a635a, + 0xbbefb5e96e0d495f, + 0x07d3a975f0ef25a2, + 0x0083fd8e7e80dae5, + ]), + c1: ZFp([ + 0xadc0fc92df64b05d, + 0x18aa270a2b1461dc, + 0x86adac6a3be4eba0, + 0x79495c4ec93da33a, + 0xe7175850a43ccaed, + 0x0b2bc2a163de1bf2, + ]), + }, + z: ZFp2 { + c0: ZFp([ + 0x760900000002fffd, + 0xebf4000bc40c0002, + 0x5f48985753c758ba, + 0x77ce585370525745, + 0x5c071a97a256ec6d, + 0x15f65ec3fa80e493, + ]), + c1: ZFp([ + 0x0000000000000000, + 0x0000000000000000, + 0x0000000000000000, + 0x0000000000000000, + 0x0000000000000000, + 0x0000000000000000, + ]), + }, +}); + +pub const G2_NEGATIVE_GENERATOR: ZG2 = ZG2::from_g2_projective(G2Projective { + x: ZFp2 { + c0: ZFp([ + 0xf5f28fa202940a10, + 0xb3f5fb2687b4961a, + 0xa1a893b53e2ae580, + 0x9894999d1a3caee9, + 0x6f67b7631863366b, + 0x058191924350bcd7, + ]), + c1: ZFp([ + 0xa5a9c0759e23f606, + 0xaaa0c59dbccd60c3, + 0x3bb17e18e2867806, + 0x1b1ab6cc8541b367, + 0xc2b6ed0ef2158547, + 0x11922a097360edf3, + ]), + }, + y: ZFp2 { + c0: ZFp([ + 0x6d8bf5079fb65e61, + 0xc52f05df531d63a5, + 0x7f4a4d344ca692c9, + 0xa887959b8577c95f, + 0x4347fe40525c8734, + 0x197d145bbaff0bb5, + ]), + c1: ZFp([ + 0x0c3e036d209afa4e, + 0x0601d8f4863f9e23, + 0xe0832636bacc0a84, + 0xeb2def362a476f84, + 0x64044f659f0ee1e9, + 0x0ed54f48d5a1caa7, + ]), + }, + z: ZFp2 { + c0: ZFp([ + 0x760900000002fffd, + 0xebf4000bc40c0002, + 0x5f48985753c758ba, + 0x77ce585370525745, + 0x5c071a97a256ec6d, + 0x15f65ec3fa80e493, + ]), + c1: ZFp([ + 0x0000000000000000, + 0x0000000000000000, + 0x0000000000000000, + 0x0000000000000000, + 0x0000000000000000, + 0x0000000000000000, + ]), + }, +}); diff --git a/constantine/src/das.rs b/constantine/src/das.rs new file mode 100644 index 000000000..b0867d820 --- /dev/null +++ b/constantine/src/das.rs @@ -0,0 +1,87 @@ +use crate::kzg_proofs::FFTSettings; +use crate::kzg_types::ZFr as BlstFr; +use kzg::{Fr, DAS}; +use std::cmp::Ordering; + +impl FFTSettings { + fn das_fft_extension_stride(&self, ab: &mut [BlstFr], stride: usize) { + match ab.len().cmp(&2_usize) { + Ordering::Less => {} + Ordering::Greater => { + let half = ab.len(); + let halfhalf = half / 2; + + for i in 0..halfhalf { + let tmp1 = ab[i].add(&ab[halfhalf + i]); + let tmp2 = ab[i].sub(&ab[halfhalf + i]); + ab[halfhalf + i] = tmp2.mul(&self.reverse_roots_of_unity[i * 2 * stride]); + ab[i] = tmp1; + } + + #[cfg(feature = "parallel")] + { + if ab.len() > 32 { + let (lo, hi) = ab.split_at_mut(halfhalf); + rayon::join( + || self.das_fft_extension_stride(hi, stride * 2), + || self.das_fft_extension_stride(lo, stride * 2), + ); + } else { + self.das_fft_extension_stride(&mut ab[..halfhalf], stride * 2); + self.das_fft_extension_stride(&mut ab[halfhalf..], stride * 2); + } + } + #[cfg(not(feature = "parallel"))] + { + self.das_fft_extension_stride(&mut ab[..halfhalf], stride * 2); + self.das_fft_extension_stride(&mut ab[halfhalf..], stride * 2); + } + for i in 0..halfhalf { + let x = ab[i]; + let y = ab[halfhalf + i]; + let y_times_root = y.mul(&self.expanded_roots_of_unity[(1 + 2 * i) * stride]); + ab[i] = x.add(&y_times_root); + ab[halfhalf + i] = x.sub(&y_times_root); + } + } + Ordering::Equal => { + let x = ab[0].add(&ab[1]); + let y = ab[0].sub(&ab[1]); + let tmp = y.mul(&self.expanded_roots_of_unity[stride]); + + ab[0] = x.add(&tmp); + ab[1] = x.sub(&tmp); + } + } + } +} + +impl DAS for FFTSettings { + fn das_fft_extension(&self, vals: &[BlstFr]) -> Result, String> { + if vals.is_empty() { + return Err(String::from("vals can not be empty")); + } + if !vals.len().is_power_of_two() { + return Err(String::from("vals lenght has to be power of 2")); + } + if vals.len() * 2 > self.max_width { + return Err(String::from( + "vals lenght * 2 has to equal or less than FFTSetings max width", + )); + } + + let mut vals = vals.to_vec(); + let stride = self.max_width / (vals.len() * 2); + + self.das_fft_extension_stride(&mut vals, stride); + + let invlen = BlstFr::from_u64(vals.len() as u64); + let invlen = invlen.inverse(); + + for val in &mut vals { + val.fr *= invlen.fr + } + + Ok(vals) + } +} diff --git a/constantine/src/eip_4844.rs b/constantine/src/eip_4844.rs new file mode 100644 index 000000000..eadc496ff --- /dev/null +++ b/constantine/src/eip_4844.rs @@ -0,0 +1,406 @@ +extern crate alloc; + +use crate::kzg_proofs::{FFTSettings, KZGSettings}; +use crate::kzg_types::{ZFr, ZG1, ZG2}; +use blst::{blst_fr, blst_p1, blst_p2}; +use kzg::common_utils::reverse_bit_order; +use kzg::eip_4844::{ + blob_to_kzg_commitment_rust, compute_blob_kzg_proof_rust, compute_kzg_proof_rust, + load_trusted_setup_rust, verify_blob_kzg_proof_batch_rust, verify_blob_kzg_proof_rust, + verify_kzg_proof_rust, Blob, Bytes32, Bytes48, CKZGSettings, KZGCommitment, KZGProof, + BYTES_PER_FIELD_ELEMENT, BYTES_PER_G1, BYTES_PER_G2, C_KZG_RET, C_KZG_RET_BADARGS, + C_KZG_RET_OK, FIELD_ELEMENTS_PER_BLOB, TRUSTED_SETUP_NUM_G1_POINTS, + TRUSTED_SETUP_NUM_G2_POINTS, +}; +use kzg::{cfg_into_iter, Fr, G1}; +use std::ptr::null_mut; + +#[cfg(feature = "std")] +use libc::FILE; +#[cfg(feature = "std")] +use std::fs::File; +#[cfg(feature = "std")] +use std::io::Read; + +#[cfg(feature = "parallel")] +use rayon::prelude::*; + +#[cfg(feature = "std")] +use kzg::eip_4844::load_trusted_setup_string; + +#[cfg(feature = "std")] +pub fn load_trusted_setup_filename_rust(filepath: &str) -> Result { + let mut file = File::open(filepath).map_err(|_| "Unable to open file".to_string())?; + let mut contents = String::new(); + file.read_to_string(&mut contents) + .map_err(|_| "Unable to read file".to_string())?; + + let (g1_bytes, g2_bytes) = load_trusted_setup_string(&contents)?; + load_trusted_setup_rust(g1_bytes.as_slice(), g2_bytes.as_slice()) +} + +fn fft_settings_to_rust(c_settings: *const CKZGSettings) -> Result { + let settings = unsafe { &*c_settings }; + let roots_of_unity = unsafe { + core::slice::from_raw_parts(settings.roots_of_unity, settings.max_width as usize) + .iter() + .map(|r| ZFr::from_blst_fr(*r)) + .collect::>() + }; + let mut expanded_roots_of_unity = roots_of_unity.clone(); + reverse_bit_order(&mut expanded_roots_of_unity)?; + expanded_roots_of_unity.push(ZFr::one()); + let mut reverse_roots_of_unity = expanded_roots_of_unity.clone(); + reverse_roots_of_unity.reverse(); + + let mut first_root = expanded_roots_of_unity[1]; + let first_root_arr = [first_root; 1]; + first_root = first_root_arr[0]; + + Ok(FFTSettings { + max_width: settings.max_width as usize, + root_of_unity: first_root, + expanded_roots_of_unity, + reverse_roots_of_unity, + roots_of_unity, + }) +} + +fn kzg_settings_to_rust(c_settings: &CKZGSettings) -> Result { + let secret_g1 = unsafe { + core::slice::from_raw_parts(c_settings.g1_values, TRUSTED_SETUP_NUM_G1_POINTS) + .iter() + .map(|r| ZG1::from_blst_p1(*r)) + .collect::>() + }; + let secret_g2 = unsafe { + core::slice::from_raw_parts(c_settings.g2_values, TRUSTED_SETUP_NUM_G2_POINTS) + .iter() + .map(|r| ZG2::from_blst_p2(*r)) + .collect::>() + }; + Ok(KZGSettings { + fs: fft_settings_to_rust(c_settings)?, + secret_g1, + secret_g2, + }) +} + +fn kzg_settings_to_c(rust_settings: &KZGSettings) -> CKZGSettings { + let g1_val = rust_settings + .secret_g1 + .iter() + .map(|r| r.to_blst_p1()) + .collect::>(); + let g1_val = Box::new(g1_val); + let g2_val = rust_settings + .secret_g2 + .iter() + .map(|r| r.to_blst_p2()) + .collect::>(); + let x = g2_val.into_boxed_slice(); + let stat_ref = Box::leak(x); + let v = Box::into_raw(g1_val); + + let roots_of_unity = Box::new( + rust_settings + .fs + .roots_of_unity + .iter() + .map(|r| r.to_blst_fr()) + .collect::>(), + ); + + CKZGSettings { + max_width: rust_settings.fs.max_width as u64, + roots_of_unity: unsafe { (*Box::into_raw(roots_of_unity)).as_mut_ptr() }, + g1_values: unsafe { (*v).as_mut_ptr() }, + g2_values: stat_ref.as_mut_ptr(), + } +} + +unsafe fn deserialize_blob(blob: *const Blob) -> Result, C_KZG_RET> { + (*blob) + .bytes + .chunks(BYTES_PER_FIELD_ELEMENT) + .map(|chunk| { + let mut bytes = [0u8; BYTES_PER_FIELD_ELEMENT]; + bytes.copy_from_slice(chunk); + if let Ok(result) = ZFr::from_bytes(&bytes) { + Ok(result) + } else { + Err(C_KZG_RET_BADARGS) + } + }) + .collect::, C_KZG_RET>>() +} + +macro_rules! handle_ckzg_badargs { + ($x: expr) => { + match $x { + Ok(value) => value, + Err(_) => return C_KZG_RET_BADARGS, + } + }; +} + +/// # Safety +#[no_mangle] +pub unsafe extern "C" fn blob_to_kzg_commitment( + out: *mut KZGCommitment, + blob: *const Blob, + s: &CKZGSettings, +) -> C_KZG_RET { + if TRUSTED_SETUP_NUM_G1_POINTS == 0 { + // FIXME: load_trusted_setup should set this value, but if not, it fails + TRUSTED_SETUP_NUM_G1_POINTS = FIELD_ELEMENTS_PER_BLOB + }; + + let deserialized_blob = handle_ckzg_badargs!(deserialize_blob(blob)); + let settings = handle_ckzg_badargs!(kzg_settings_to_rust(s)); + let tmp = handle_ckzg_badargs!(blob_to_kzg_commitment_rust(&deserialized_blob, &settings)); + + (*out).bytes = tmp.to_bytes(); + C_KZG_RET_OK +} + +/// # Safety +#[no_mangle] +pub unsafe extern "C" fn load_trusted_setup( + out: *mut CKZGSettings, + g1_bytes: *const u8, + n1: usize, + g2_bytes: *const u8, + n2: usize, +) -> C_KZG_RET { + let g1_bytes = core::slice::from_raw_parts(g1_bytes, n1 * BYTES_PER_G1); + let g2_bytes = core::slice::from_raw_parts(g2_bytes, n2 * BYTES_PER_G2); + TRUSTED_SETUP_NUM_G1_POINTS = g1_bytes.len() / BYTES_PER_G1; + let settings = handle_ckzg_badargs!(load_trusted_setup_rust(g1_bytes, g2_bytes)); + + *out = kzg_settings_to_c(&settings); + C_KZG_RET_OK +} + +/// # Safety +#[cfg(feature = "std")] +#[no_mangle] +pub unsafe extern "C" fn load_trusted_setup_file( + out: *mut CKZGSettings, + in_: *mut FILE, +) -> C_KZG_RET { + let mut buf = vec![0u8; 1024 * 1024]; + let len: usize = libc::fread(buf.as_mut_ptr() as *mut libc::c_void, 1, buf.len(), in_); + let s = handle_ckzg_badargs!(String::from_utf8(buf[..len].to_vec())); + let (g1_bytes, g2_bytes) = handle_ckzg_badargs!(load_trusted_setup_string(&s)); + TRUSTED_SETUP_NUM_G1_POINTS = g1_bytes.len() / BYTES_PER_G1; + if TRUSTED_SETUP_NUM_G1_POINTS != FIELD_ELEMENTS_PER_BLOB { + // Helps pass the Java test "shouldThrowExceptionOnIncorrectTrustedSetupFromFile", + // as well as 5 others that pass only if this one passes (likely because Java doesn't + // deallocate its KZGSettings pointer when no exception is thrown). + return C_KZG_RET_BADARGS; + } + let settings = handle_ckzg_badargs!(load_trusted_setup_rust( + g1_bytes.as_slice(), + g2_bytes.as_slice() + )); + + *out = kzg_settings_to_c(&settings); + C_KZG_RET_OK +} + +/// # Safety +#[no_mangle] +pub unsafe extern "C" fn free_trusted_setup(s: *mut CKZGSettings) { + if s.is_null() { + return; + } + + let max_width = (*s).max_width as usize; + let roots = Box::from_raw(core::slice::from_raw_parts_mut( + (*s).roots_of_unity, + max_width, + )); + drop(roots); + (*s).roots_of_unity = null_mut(); + + let g1 = Box::from_raw(core::slice::from_raw_parts_mut( + (*s).g1_values, + TRUSTED_SETUP_NUM_G1_POINTS, + )); + drop(g1); + (*s).g1_values = null_mut(); + + let g2 = Box::from_raw(core::slice::from_raw_parts_mut( + (*s).g2_values, + TRUSTED_SETUP_NUM_G2_POINTS, + )); + drop(g2); + (*s).g2_values = null_mut(); + (*s).max_width = 0; +} + +/// # Safety +#[no_mangle] +pub unsafe extern "C" fn verify_kzg_proof( + ok: *mut bool, + commitment_bytes: *const Bytes48, + z_bytes: *const Bytes32, + y_bytes: *const Bytes32, + proof_bytes: *const Bytes48, + s: &CKZGSettings, +) -> C_KZG_RET { + let frz = handle_ckzg_badargs!(ZFr::from_bytes(&(*z_bytes).bytes)); + let fry = handle_ckzg_badargs!(ZFr::from_bytes(&(*y_bytes).bytes)); + let g1commitment = handle_ckzg_badargs!(ZG1::from_bytes(&(*commitment_bytes).bytes)); + let g1proof = handle_ckzg_badargs!(ZG1::from_bytes(&(*proof_bytes).bytes)); + + let settings = handle_ckzg_badargs!(kzg_settings_to_rust(s)); + + let result = handle_ckzg_badargs!(verify_kzg_proof_rust( + &g1commitment, + &frz, + &fry, + &g1proof, + &settings + )); + + *ok = result; + C_KZG_RET_OK +} + +/// # Safety +#[no_mangle] +pub unsafe extern "C" fn verify_blob_kzg_proof( + ok: *mut bool, + blob: *const Blob, + commitment_bytes: *const Bytes48, + proof_bytes: *const Bytes48, + s: &CKZGSettings, +) -> C_KZG_RET { + let deserialized_blob = handle_ckzg_badargs!(deserialize_blob(blob)); + + let commitment_g1 = handle_ckzg_badargs!(ZG1::from_bytes(&(*commitment_bytes).bytes)); + let proof_g1 = handle_ckzg_badargs!(ZG1::from_bytes(&(*proof_bytes).bytes)); + + let settings = handle_ckzg_badargs!(kzg_settings_to_rust(s)); + + let result = handle_ckzg_badargs!(verify_blob_kzg_proof_rust( + &deserialized_blob, + &commitment_g1, + &proof_g1, + &settings, + )); + + *ok = result; + C_KZG_RET_OK +} + +/// # Safety +#[no_mangle] +pub unsafe extern "C" fn verify_blob_kzg_proof_batch( + ok: *mut bool, + blobs: *const Blob, + commitments_bytes: *const Bytes48, + proofs_bytes: *const Bytes48, + n: usize, + s: &CKZGSettings, +) -> C_KZG_RET { + let raw_blobs = core::slice::from_raw_parts(blobs, n); + let raw_commitments = core::slice::from_raw_parts(commitments_bytes, n); + let raw_proofs = core::slice::from_raw_parts(proofs_bytes, n); + + let deserialized_blobs: Result>, C_KZG_RET> = cfg_into_iter!(raw_blobs) + .map(|raw_blob| deserialize_blob(raw_blob).map_err(|_| C_KZG_RET_BADARGS)) + .collect(); + + let commitments_g1: Result, C_KZG_RET> = cfg_into_iter!(raw_commitments) + .map(|raw_commitment| ZG1::from_bytes(&raw_commitment.bytes).map_err(|_| C_KZG_RET_BADARGS)) + .collect(); + + let proofs_g1: Result, C_KZG_RET> = cfg_into_iter!(raw_proofs) + .map(|raw_proof| ZG1::from_bytes(&raw_proof.bytes).map_err(|_| C_KZG_RET_BADARGS)) + .collect(); + + if let (Ok(blobs), Ok(commitments), Ok(proofs)) = + (deserialized_blobs, commitments_g1, proofs_g1) + { + let settings = match kzg_settings_to_rust(s) { + Ok(value) => value, + Err(_) => return C_KZG_RET_BADARGS, + }; + + let result = + verify_blob_kzg_proof_batch_rust(blobs.as_slice(), &commitments, &proofs, &settings); + + if let Ok(result) = result { + *ok = result; + C_KZG_RET_OK + } else { + C_KZG_RET_BADARGS + } + } else { + *ok = false; + C_KZG_RET_BADARGS + } +} + +/// # Safety +#[no_mangle] +pub unsafe extern "C" fn compute_blob_kzg_proof( + out: *mut KZGProof, + blob: *const Blob, + commitment_bytes: *const Bytes48, + s: &CKZGSettings, +) -> C_KZG_RET { + let deserialized_blob = match deserialize_blob(blob) { + Ok(value) => value, + Err(err) => return err, + }; + + let commitment_g1 = handle_ckzg_badargs!(ZG1::from_bytes(&(*commitment_bytes).bytes)); + let settings = handle_ckzg_badargs!(kzg_settings_to_rust(s)); + let proof = handle_ckzg_badargs!(compute_blob_kzg_proof_rust( + &deserialized_blob, + &commitment_g1, + &settings + )); + + (*out).bytes = proof.to_bytes(); + C_KZG_RET_OK +} + +/// # Safety +#[no_mangle] +pub unsafe extern "C" fn compute_kzg_proof( + proof_out: *mut KZGProof, + y_out: *mut Bytes32, + blob: *const Blob, + z_bytes: *const Bytes32, + s: &CKZGSettings, +) -> C_KZG_RET { + let deserialized_blob = match deserialize_blob(blob) { + Ok(value) => value, + Err(err) => return err, + }; + + let frz = match ZFr::from_bytes(&(*z_bytes).bytes) { + Ok(value) => value, + Err(_) => return C_KZG_RET_BADARGS, + }; + + let settings = match kzg_settings_to_rust(s) { + Ok(value) => value, + Err(_) => return C_KZG_RET_BADARGS, + }; + + let (proof_out_tmp, fry_tmp) = match compute_kzg_proof_rust(&deserialized_blob, &frz, &settings) + { + Ok(value) => value, + Err(_) => return C_KZG_RET_BADARGS, + }; + + (*proof_out).bytes = proof_out_tmp.to_bytes(); + (*y_out).bytes = fry_tmp.to_bytes(); + C_KZG_RET_OK +} diff --git a/constantine/src/fft.rs b/constantine/src/fft.rs new file mode 100644 index 000000000..db3fb7dfb --- /dev/null +++ b/constantine/src/fft.rs @@ -0,0 +1,106 @@ +use crate::kzg_proofs::FFTSettings; +use crate::kzg_types::ZFr as BlstFr; +use kzg::{FFTFr, Fr as FFr}; + +impl FFTFr for FFTSettings { + fn fft_fr(&self, data: &[BlstFr], inverse: bool) -> Result, String> { + if data.len() > self.max_width { + return Err(String::from("data length is longer than allowed max width")); + } + if !data.len().is_power_of_two() { + return Err(String::from("data length is not power of 2")); + } + + let stride = self.max_width / data.len(); + let mut ret = vec![BlstFr::default(); data.len()]; + + let roots = if inverse { + &self.reverse_roots_of_unity + } else { + &self.expanded_roots_of_unity + }; + + fft_fr_fast(&mut ret, data, 1, roots, stride); + + if inverse { + let inv_fr_len = BlstFr::from_u64(data.len() as u64).inverse(); + ret[..data.len()] + .iter_mut() + .for_each(|f| *f = BlstFr::mul(f, &inv_fr_len)); + } + + Ok(ret) + } +} +pub fn fft_fr_fast( + ret: &mut [BlstFr], + data: &[BlstFr], + stride: usize, + roots: &[BlstFr], + roots_stride: usize, +) { + let half: usize = ret.len() / 2; + if half > 0 { + #[cfg(not(feature = "parallel"))] + { + fft_fr_fast(&mut ret[..half], data, stride * 2, roots, roots_stride * 2); + fft_fr_fast( + &mut ret[half..], + &data[stride..], + stride * 2, + roots, + roots_stride * 2, + ); + } + + #[cfg(feature = "parallel")] + { + if half > 256 { + let (lo, hi) = ret.split_at_mut(half); + rayon::join( + || fft_fr_fast(lo, data, stride * 2, roots, roots_stride * 2), + || fft_fr_fast(hi, &data[stride..], stride * 2, roots, roots_stride * 2), + ); + } else { + fft_fr_fast(&mut ret[..half], data, stride * 2, roots, roots_stride * 2); + fft_fr_fast( + &mut ret[half..], + &data[stride..], + stride * 2, + roots, + roots_stride * 2, + ); + } + } + + for i in 0..half { + let y_times_root = ret[i + half].mul(&roots[i * roots_stride]); + ret[i + half] = ret[i].sub(&y_times_root); + ret[i] = ret[i].add(&y_times_root); + } + } else { + ret[0] = data[0]; + } +} + +pub fn fft_fr_slow( + ret: &mut [BlstFr], + data: &[BlstFr], + stride: usize, + roots: &[BlstFr], + roots_stride: usize, +) { + let mut v; + let mut jv; + let mut r; + + for i in 0..data.len() { + ret[i] = data[0].mul(&roots[0]); + for j in 1..data.len() { + jv = data[j * stride]; + r = roots[((i * j) % data.len()) * roots_stride]; + v = jv.mul(&r); + ret[i] = ret[i].add(&v); + } + } +} diff --git a/constantine/src/fft_g1.rs b/constantine/src/fft_g1.rs new file mode 100644 index 000000000..8299866d2 --- /dev/null +++ b/constantine/src/fft_g1.rs @@ -0,0 +1,122 @@ +use crate::consts::G1_GENERATOR; +use crate::kzg_proofs::FFTSettings; +use crate::kzg_types::{ZFr, ZG1}; +use crate::multiscalar_mul::msm_variable_base; +use kzg::{Fr as KzgFr, G1Mul}; +use kzg::{FFTG1, G1}; +use std::ops::MulAssign; + +#[warn(unused_variables)] +pub fn g1_linear_combination(out: &mut ZG1, points: &[ZG1], scalars: &[ZFr], _len: usize) { + let g1 = msm_variable_base(points, scalars); + out.proj = g1 +} +pub fn make_data(data: usize) -> Vec { + let mut vec = Vec::new(); + if data != 0 { + vec.push(G1_GENERATOR); + for i in 1..data as u64 { + let res = vec[(i - 1) as usize].add_or_dbl(&G1_GENERATOR); + vec.push(res); + } + } + vec +} + +impl FFTG1 for FFTSettings { + fn fft_g1(&self, data: &[ZG1], inverse: bool) -> Result, String> { + if data.len() > self.max_width { + return Err(String::from("data length is longer than allowed max width")); + } + if !data.len().is_power_of_two() { + return Err(String::from("data length is not power of 2")); + } + + let stride: usize = self.max_width / data.len(); + let mut ret = vec![ZG1::default(); data.len()]; + + let roots = if inverse { + &self.reverse_roots_of_unity + } else { + &self.expanded_roots_of_unity + }; + + fft_g1_fast(&mut ret, data, 1, roots, stride, 1); + + if inverse { + let inv_fr_len = ZFr::from_u64(data.len() as u64).inverse(); + ret[..data.len()] + .iter_mut() + .for_each(|f| f.proj.mul_assign(&inv_fr_len.fr)); + } + Ok(ret) + } +} + +pub fn fft_g1_slow( + ret: &mut [ZG1], + data: &[ZG1], + stride: usize, + roots: &[ZFr], + roots_stride: usize, + _width: usize, +) { + for i in 0..data.len() { + ret[i] = data[0].mul(&roots[0]); + for j in 1..data.len() { + let jv = data[j * stride]; + let r = roots[((i * j) % data.len()) * roots_stride]; + let v = jv.mul(&r); + ret[i] = ret[i].add_or_dbl(&v); + } + } +} + +pub fn fft_g1_fast( + ret: &mut [ZG1], + data: &[ZG1], + stride: usize, + roots: &[ZFr], + roots_stride: usize, + _width: usize, +) { + let half = ret.len() / 2; + if half > 0 { + #[cfg(feature = "parallel")] + { + let (lo, hi) = ret.split_at_mut(half); + rayon::join( + || fft_g1_fast(hi, &data[stride..], stride * 2, roots, roots_stride * 2, 1), + || fft_g1_fast(lo, data, stride * 2, roots, roots_stride * 2, 1), + ); + } + + #[cfg(not(feature = "parallel"))] + { + fft_g1_fast( + &mut ret[..half], + data, + stride * 2, + roots, + roots_stride * 2, + 1, + ); + fft_g1_fast( + &mut ret[half..], + &data[stride..], + stride * 2, + roots, + roots_stride * 2, + 1, + ); + } + + for i in 0..half { + let y_times_root = ret[i + half].mul(&roots[i * roots_stride]); + ret[i + half] = ret[i].sub(&y_times_root); + ret[i] = ret[i].add_or_dbl(&y_times_root); + } + } else { + ret[0] = data[0]; + } +} diff --git a/constantine/src/fk20_proofs.rs b/constantine/src/fk20_proofs.rs new file mode 100644 index 000000000..ec3917f63 --- /dev/null +++ b/constantine/src/fk20_proofs.rs @@ -0,0 +1,324 @@ +use crate::consts::G1_IDENTITY; +use crate::kzg_proofs::{FFTSettings, KZGSettings}; +use crate::kzg_types::{ZFr as BlstFr, ZG1, ZG2}; +use crate::poly::PolyData; +use kzg::common_utils::reverse_bit_order; +use kzg::{FFTFr, FK20MultiSettings, FK20SingleSettings, Fr, G1Mul, Poly, FFTG1, G1}; + +#[cfg(feature = "parallel")] +use rayon::prelude::*; + +#[repr(C)] +#[derive(Debug, Clone, Default)] +pub struct KzgFK20SingleSettings { + pub ks: KZGSettings, + pub x_ext_fft: Vec, + pub x_ext_fft_len: usize, +} +#[repr(C)] +#[derive(Debug, Clone, Default)] +pub struct KzgFK20MultiSettings { + pub ks: KZGSettings, + pub chunk_len: usize, + pub x_ext_fft_files: Vec>, + pub length: usize, +} + +impl FK20SingleSettings + for KzgFK20SingleSettings +{ + fn new(ks: &KZGSettings, n2: usize) -> Result { + let n = n2 / 2; + + if n2 > ks.fs.max_width { + return Err(String::from( + "n2 must be equal or less than kzg settings max width", + )); + } + if !n2.is_power_of_two() { + return Err(String::from("n2 must be power of 2")); + } + if n2 < 2 { + return Err(String::from("n2 must be equal or greater than 2")); + } + + let mut x = Vec::new(); + for i in 0..(n - 1) { + x.push(ks.secret_g1[n - 2 - i]) + } + x.push(G1_IDENTITY); + + let new_ks = KZGSettings { + fs: ks.fs.clone(), + ..KZGSettings::default() + }; + + Ok(KzgFK20SingleSettings { + ks: new_ks, + x_ext_fft: toeplitz_part_1(&x, &ks.fs).unwrap(), + x_ext_fft_len: n2, + }) + } + + fn data_availability(&self, p: &PolyData) -> Result, String> { + let n = p.len(); + let n2 = n * 2; + + if n2 > self.ks.fs.max_width { + return Err(String::from( + "n2 must be equal or less than kzg settings max width", + )); + } + if !n.is_power_of_two() { + return Err(String::from("n2 must be power of 2")); + } + + let mut out = fk20_single_da_opt(p, self).unwrap(); + reverse_bit_order(&mut out)?; + Ok(out) + } + + fn data_availability_optimized(&self, p: &PolyData) -> Result, String> { + fk20_single_da_opt(p, self) + } +} + +impl FK20MultiSettings + for KzgFK20MultiSettings +{ + fn new(ks: &KZGSettings, n2: usize, chunk_len: usize) -> Result { + if n2 > ks.fs.max_width { + return Err(String::from( + "n2 must be equal or less than kzg settings max width", + )); + } + if !n2.is_power_of_two() { + return Err(String::from("n2 must be power of 2")); + } + if n2 < 2 { + return Err(String::from("n2 must be equal or greater than 2")); + } + if chunk_len > n2 / 2 { + return Err(String::from("chunk_len must be equal or less than n2/2")); + } + if !chunk_len.is_power_of_two() { + return Err(String::from("chunk_len must be power of 2")); + } + if chunk_len == 0 { + return Err(String::from("chunk_len must be greater than 0")); + } + + let n = n2 / 2; + let k = n / chunk_len; + + let mut x_ext_fft_files = Vec::new(); + + for offset in 0..chunk_len { + let mut x = vec![ZG1::default(); k]; + let start = if n >= chunk_len + 1 + offset { + n - chunk_len - 1 - offset + } else { + 0 + }; + let mut j = start; + for i in x.iter_mut().take(k - 1) { + i.proj = ks.secret_g1[j].proj; + if j >= chunk_len { + j -= chunk_len; + } else { + j = 0; + } + } + x[k - 1] = G1_IDENTITY; + x_ext_fft_files.push(toeplitz_part_1(&x, &ks.fs).unwrap()); + } + + let new_ks = KZGSettings { + fs: ks.fs.clone(), + ..KZGSettings::default() + }; + + Ok(KzgFK20MultiSettings { + ks: new_ks, + x_ext_fft_files, + chunk_len, + length: n, //unsure if this is right + }) + } + + fn data_availability(&self, p: &PolyData) -> Result, String> { + let n = p.len(); + let n2 = n * 2; + + if n2 > self.ks.fs.max_width { + return Err(String::from( + "n2 must be equal or less than kzg settings max width", + )); + } + if !n.is_power_of_two() { + return Err(String::from("n2 must be power of 2")); + } + + let mut out = fk20_multi_da_opt(p, self).unwrap(); + reverse_bit_order(&mut out)?; + Ok(out) + } + + fn data_availability_optimized(&self, p: &PolyData) -> Result, String> { + fk20_multi_da_opt(p, self) + } +} + +fn fk20_single_da_opt(p: &PolyData, fk: &KzgFK20SingleSettings) -> Result, String> { + let n = p.len(); + let n2 = n * 2; + + if n2 > fk.ks.fs.max_width { + return Err(String::from( + "n2 must be equal or less than kzg settings max width", + )); + } + if !n.is_power_of_two() { + return Err(String::from("n2 must be power of 2")); + } + + let outlen = 2 * p.len(); + let toeplitz_coeffs = toeplitz_coeffs_step(p, outlen).unwrap(); + let h_ext_fft = toeplitz_part_2(&toeplitz_coeffs, &fk.x_ext_fft, &fk.ks.fs).unwrap(); + let h = toeplitz_part_3(&h_ext_fft, &fk.ks.fs).unwrap(); + + fk.ks.fs.fft_g1(&h, false) +} + +fn fk20_multi_da_opt(p: &PolyData, fk: &KzgFK20MultiSettings) -> Result, String> { + let n = p.len(); + let n2 = n * 2; + + if n2 > fk.ks.fs.max_width { + return Err(String::from( + "n2 must be equal or less than kzg settings max width", + )); + } + if !n.is_power_of_two() { + return Err(String::from("n2 must be power of 2")); + } + + let n = n2 / 2; + let k = n / fk.chunk_len; + let k2 = k * 2; + + let mut h_ext_fft = Vec::new(); + for _i in 0..k2 { + h_ext_fft.push(G1_IDENTITY); + } + + let mut toeplitz_coeffs = PolyData::new(n2 / fk.chunk_len); + for i in 0..fk.chunk_len { + toeplitz_coeffs = + toeplitz_coeffs_stride(p, i, fk.chunk_len, toeplitz_coeffs.len()).unwrap(); + let h_ext_fft_file = + toeplitz_part_2(&toeplitz_coeffs, &fk.x_ext_fft_files[i], &fk.ks.fs).unwrap(); + for j in 0..k2 { + h_ext_fft[j] = h_ext_fft[j].add_or_dbl(&h_ext_fft_file[j]); + } + } + + // Calculate `h` + let mut h = toeplitz_part_3(&h_ext_fft, &fk.ks.fs).unwrap(); + + // Overwrite the second half of `h` with zero + for i in h.iter_mut().take(k2).skip(k) { + i.proj = G1_IDENTITY.proj; + } + + fk.ks.fs.fft_g1(&h, false) +} + +fn toeplitz_coeffs_step(p: &PolyData, outlen: usize) -> Result { + toeplitz_coeffs_stride(p, 0, 1, outlen) +} + +fn toeplitz_coeffs_stride( + poly: &PolyData, + offset: usize, + stride: usize, + outlen: usize, +) -> Result { + let n = poly.len(); + + if stride == 0 { + return Err(String::from("stride must be greater than 0")); + } + + let k = n / stride; + let k2 = k * 2; + + if outlen < k2 { + return Err(String::from("outlen must be equal or greater than k2")); + } + + let mut out = PolyData::new(outlen); + out.set_coeff_at(0, &poly.coeffs[n - 1 - offset]); + let mut i = 1; + while i <= (k + 1) && i < k2 { + out.set_coeff_at(i, &BlstFr::zero()); + i += 1; + } + let mut j = 2 * stride - offset - 1; + for i in (k + 2)..k2 { + out.set_coeff_at(i, &poly.coeffs[j]); + j += stride; + } + Ok(out) +} + +fn toeplitz_part_1(x: &[ZG1], fs: &FFTSettings) -> Result, String> { + let n = x.len(); + let n2 = n * 2; + + let mut x_ext = Vec::new(); + for i in x.iter().take(n) { + x_ext.push(*i); + } + for _i in n..n2 { + x_ext.push(G1_IDENTITY); + } + fs.fft_g1(&x_ext, false) +} + +fn toeplitz_part_2( + toeplitz_coeffs: &PolyData, + x_ext_fft: &[ZG1], + fs: &FFTSettings, +) -> Result, String> { + let toeplitz_coeffs_fft = fs.fft_fr(&toeplitz_coeffs.coeffs, false).unwrap(); + + #[cfg(feature = "parallel")] + { + let out: Vec<_> = (0..toeplitz_coeffs.len()) + .into_par_iter() + .map(|i| x_ext_fft[i].mul(&toeplitz_coeffs_fft[i])) + .collect(); + Ok(out) + } + + #[cfg(not(feature = "parallel"))] + { + let mut out = Vec::new(); + for i in 0..toeplitz_coeffs.len() { + out.push(x_ext_fft[i].mul(&toeplitz_coeffs_fft[i])); + } + Ok(out) + } +} + +fn toeplitz_part_3(h_ext_fft: &[ZG1], fs: &FFTSettings) -> Result, String> { + let n = h_ext_fft.len() / 2; + let mut out = fs.fft_g1(h_ext_fft, true).unwrap(); + + // Zero the second half of h + for i in out.iter_mut().take(h_ext_fft.len()).skip(n) { + i.proj = G1_IDENTITY.proj; + } + Ok(out) +} diff --git a/constantine/src/kzg_proofs.rs b/constantine/src/kzg_proofs.rs new file mode 100644 index 000000000..fd2092746 --- /dev/null +++ b/constantine/src/kzg_proofs.rs @@ -0,0 +1,105 @@ +#![allow(non_camel_case_types)] +use crate::consts::{G1_GENERATOR, G2_GENERATOR}; +use crate::kzg_types::ZFr; +use crate::kzg_types::{ZFr as BlstFr, ZG1, ZG2}; +use crate::poly::PolyData; +use bls12_381::{ + multi_miller_loop, Fp12 as ZFp12, G1Affine, G2Affine, G2Prepared, MillerLoopResult, +}; +use kzg::eip_4844::hash_to_bls_field; +use kzg::{Fr as FrTrait, G1Mul, G2Mul}; +use std::ops::{Add, Neg}; + +#[derive(Debug, Clone)] +pub struct FFTSettings { + pub max_width: usize, + pub root_of_unity: BlstFr, + pub expanded_roots_of_unity: Vec, + pub reverse_roots_of_unity: Vec, + pub roots_of_unity: Vec, +} + +pub fn expand_root_of_unity(root: &BlstFr, width: usize) -> Result, String> { + let mut generated_powers = vec![BlstFr::one(), *root]; + + while !(generated_powers.last().unwrap().is_one()) { + if generated_powers.len() > width { + return Err(String::from("Root of unity multiplied for too long")); + } + + generated_powers.push(generated_powers.last().unwrap().mul(root)); + } + + if generated_powers.len() != width + 1 { + return Err(String::from("Root of unity has invalid scale")); + } + + Ok(generated_powers) +} + +#[derive(Debug, Clone, Default)] +pub struct KZGSettings { + pub fs: FFTSettings, + pub secret_g1: Vec, + pub secret_g2: Vec, +} + +pub fn generate_trusted_setup(len: usize, secret: [u8; 32usize]) -> (Vec, Vec) { + let s = hash_to_bls_field::(&secret); + let mut s_pow = ZFr::one(); + + let mut s1 = Vec::with_capacity(len); + let mut s2 = Vec::with_capacity(len); + + for _ in 0..len { + s1.push(G1_GENERATOR.mul(&s_pow)); + s2.push(G2_GENERATOR.mul(&s_pow)); + + s_pow = s_pow.mul(&s); + } + + (s1, s2) +} + +pub fn eval_poly(p: &PolyData, x: &ZFr) -> ZFr { + if p.coeffs.is_empty() { + return ZFr::zero(); + } else if x.is_zero() { + return p.coeffs[0]; + } + + let mut out = p.coeffs[p.coeffs.len() - 1]; + let mut i = p.coeffs.len() - 2; + + loop { + let temp = out.mul(x); + out = temp.add(&p.coeffs[i]); + + if i == 0 { + break; + } + i -= 1; + } + out +} + +pub fn pairings_verify(a1: &ZG1, a2: &ZG2, b1: &ZG1, b2: &ZG2) -> bool { + let a1neg = a1.proj.neg(); + + let aa1 = G1Affine::from(&a1neg); + let bb1 = G1Affine::from(b1.proj); + let aa2 = G2Affine::from(a2.proj); + let bb2 = G2Affine::from(b2.proj); + + let aa2_prepared = G2Prepared::from(aa2); + let bb2_prepared = G2Prepared::from(bb2); + + let loop0 = multi_miller_loop(&[(&aa1, &aa2_prepared)]); + let loop1 = multi_miller_loop(&[(&bb1, &bb2_prepared)]); + + let gt_point = loop0.add(loop1); + + let new_point = MillerLoopResult::final_exponentiation(>_point); + + ZFp12::eq(&ZFp12::one(), &new_point.0) +} diff --git a/constantine/src/kzg_types.rs b/constantine/src/kzg_types.rs new file mode 100644 index 000000000..fbfae1193 --- /dev/null +++ b/constantine/src/kzg_types.rs @@ -0,0 +1,745 @@ +use crate::consts::{ + G1_GENERATOR, G1_IDENTITY, G1_NEGATIVE_GENERATOR, G2_GENERATOR, G2_NEGATIVE_GENERATOR, + SCALE2_ROOT_OF_UNITY, +}; +use crate::fft_g1::g1_linear_combination; +use crate::kzg_proofs::{ + expand_root_of_unity, pairings_verify, FFTSettings as ZFFTSettings, KZGSettings as ZKZGSettings, +}; +use crate::poly::PolyData; +use crate::utils::{ + blst_fr_into_pc_fr, blst_p1_into_pc_g1projective, blst_p2_into_pc_g2projective, + pc_fr_into_blst_fr, pc_g1projective_into_blst_p1, pc_g2projective_into_blst_p2, +}; +use bls12_381::{G1Affine, G1Projective, G2Affine, G2Projective, Scalar, MODULUS, R2}; +use blst::{blst_fr, blst_p1}; +use ff::Field; +use kzg::common_utils::reverse_bit_order; +use kzg::eip_4844::{BYTES_PER_FIELD_ELEMENT, BYTES_PER_G1, BYTES_PER_G2}; +use kzg::{ + FFTFr, FFTSettings, Fr as KzgFr, G1Mul, G2Mul, KZGSettings, PairingVerify, Poly, G1, G2, +}; +use std::ops::{Mul, Sub}; + +use ff::derive::sbb; +use subtle::{ConstantTimeEq, CtOption}; + +fn to_scalar(zfr: &ZFr) -> Scalar { + zfr.fr +} + +fn bigint_check_mod_256(a: &[u64; 4]) -> bool { + let (_, overflow) = a[0].overflowing_sub(MODULUS.0[0]); + let (_, overflow) = a[1].overflowing_sub(MODULUS.0[1] + overflow as u64); + let (_, overflow) = a[2].overflowing_sub(MODULUS.0[2] + overflow as u64); + let (_, overflow) = a[3].overflowing_sub(MODULUS.0[3] + overflow as u64); + overflow +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq, Default)] +pub struct ZFr { + pub fr: Scalar, +} + +impl ZFr { + pub fn from_blst_fr(fr: blst_fr) -> Self { + Self { + fr: blst_fr_into_pc_fr(fr), + } + } + pub fn to_blst_fr(&self) -> blst_fr { + pc_fr_into_blst_fr(self.fr) + } + + pub fn converter(points: &[ZFr]) -> Vec { + let mut result = Vec::new(); + + for zg1 in points { + result.push(zg1.fr); + } + result + } +} + +impl KzgFr for ZFr { + fn null() -> Self { + Self { + fr: Scalar([u64::MAX, u64::MAX, u64::MAX, u64::MAX]), + } + } + fn zero() -> Self { + Self::from_u64(0) + } + + fn one() -> Self { + Self::from_u64(1) + } + + #[cfg(feature = "rand")] + fn rand() -> Self { + let rng = rand::thread_rng(); + let rusult = ff::Field::random(rng); + Self { fr: rusult } + } + #[allow(clippy::bind_instead_of_map)] + fn from_bytes(bytes: &[u8]) -> Result { + bytes + .try_into() + .map_err(|_| { + format!( + "Invalid byte length. Expected {}, got {}", + BYTES_PER_FIELD_ELEMENT, + bytes.len() + ) + }) + .and_then(|bytes: &[u8; BYTES_PER_FIELD_ELEMENT]| { + let mut tmp = Scalar([0, 0, 0, 0]); + + tmp.0[0] = u64::from_be_bytes(<[u8; 8]>::try_from(&bytes[0..8]).unwrap()); + tmp.0[1] = u64::from_be_bytes(<[u8; 8]>::try_from(&bytes[8..16]).unwrap()); + tmp.0[2] = u64::from_be_bytes(<[u8; 8]>::try_from(&bytes[16..24]).unwrap()); + tmp.0[3] = u64::from_be_bytes(<[u8; 8]>::try_from(&bytes[24..32]).unwrap()); + + // Try to subtract the modulus + let (_, borrow) = sbb(tmp.0[0], MODULUS.0[0], 0); + let (_, borrow) = sbb(tmp.0[1], MODULUS.0[1], borrow); + let (_, borrow) = sbb(tmp.0[2], MODULUS.0[2], borrow); + let (_, _borrow) = sbb(tmp.0[3], MODULUS.0[3], borrow); + let mut tmp2 = Scalar::default(); + + tmp2.0[0] = tmp.0[3]; + tmp2.0[1] = tmp.0[2]; + tmp2.0[2] = tmp.0[1]; + tmp2.0[3] = tmp.0[0]; + + let is_zero: bool = tmp2.is_zero().into(); + if !is_zero && !bigint_check_mod_256(&tmp2.0) { + return Err("Invalid scalar".to_string()); + } + + tmp2 *= &R2; + Ok(Self { fr: tmp2 }) + }) + } + fn from_bytes_unchecked(bytes: &[u8]) -> Result { + bytes + .try_into() + .map_err(|_| { + format!( + "Invalid byte length. Expected {}, got {}", + BYTES_PER_FIELD_ELEMENT, + bytes.len() + ) + }) + .map(|bytes: &[u8; BYTES_PER_FIELD_ELEMENT]| { + let mut tmp = Scalar([0, 0, 0, 0]); + + tmp.0[0] = u64::from_be_bytes(<[u8; 8]>::try_from(&bytes[0..8]).unwrap()); + tmp.0[1] = u64::from_be_bytes(<[u8; 8]>::try_from(&bytes[8..16]).unwrap()); + tmp.0[2] = u64::from_be_bytes(<[u8; 8]>::try_from(&bytes[16..24]).unwrap()); + tmp.0[3] = u64::from_be_bytes(<[u8; 8]>::try_from(&bytes[24..32]).unwrap()); + + // Try to subtract the modulus + let (_, borrow) = sbb(tmp.0[0], MODULUS.0[0], 0); + let (_, borrow) = sbb(tmp.0[1], MODULUS.0[1], borrow); + let (_, borrow) = sbb(tmp.0[2], MODULUS.0[2], borrow); + let (_, _borrow) = sbb(tmp.0[3], MODULUS.0[3], borrow); + let mut tmp2 = Scalar::default(); + + tmp2.0[0] = tmp.0[3]; + tmp2.0[1] = tmp.0[2]; + tmp2.0[2] = tmp.0[1]; + tmp2.0[3] = tmp.0[0]; + + tmp2 *= &R2; + Self { fr: tmp2 } + }) + } + + fn from_hex(hex: &str) -> Result { + let bytes = hex::decode(&hex[2..]).unwrap(); + Self::from_bytes(&bytes) + } + + fn from_u64_arr(u: &[u64; 4]) -> Self { + Self { + fr: Scalar::from_raw(*u), + } + } + + fn from_u64(val: u64) -> Self { + Self { + fr: Scalar::from(val), + } + } + + fn to_bytes(&self) -> [u8; 32] { + let scalar = self.fr; + let tmp = Scalar::montgomery_reduce( + scalar.0[0], + scalar.0[1], + scalar.0[2], + scalar.0[3], + 0, + 0, + 0, + 0, + ); + let mut res = [0; 32]; + res[0..8].copy_from_slice(&tmp.0[3].to_be_bytes()); + res[8..16].copy_from_slice(&tmp.0[2].to_be_bytes()); + res[16..24].copy_from_slice(&tmp.0[1].to_be_bytes()); + res[24..32].copy_from_slice(&tmp.0[0].to_be_bytes()); + res + } + + //testuoti + fn to_u64_arr(&self) -> [u64; 4] { + let bytes = self.to_bytes(); + [ + u64::from_be_bytes(bytes[24..32].try_into().unwrap()), + u64::from_be_bytes(bytes[16..24].try_into().unwrap()), + u64::from_be_bytes(bytes[8..16].try_into().unwrap()), + u64::from_be_bytes(bytes[0..8].try_into().unwrap()), + ] + } + + fn is_one(&self) -> bool { + self.fr.ct_eq(&ZFr::one().fr).unwrap_u8() == 1 + } + + fn is_zero(&self) -> bool { + self.fr.is_zero().unwrap_u8() == 1 + } + + fn is_null(&self) -> bool { + self.fr.ct_eq(&ZFr::null().fr).unwrap_u8() == 1 + } + + fn sqr(&self) -> Self { + Self { + fr: self.fr.square(), + } + } + + fn mul(&self, b: &Self) -> Self { + Self { + fr: Scalar::mul(&to_scalar(self), &to_scalar(b)), + } + } + + fn add(&self, b: &Self) -> Self { + Self { fr: self.fr + b.fr } + } + + fn sub(&self, b: &Self) -> Self { + Self { fr: self.fr - b.fr } + } + + fn eucl_inverse(&self) -> Self { + Self { + fr: self.fr.invert().unwrap(), + } + } + + fn negate(&self) -> Self { + Self { fr: self.fr.neg() } + } + + fn inverse(&self) -> Self { + Self { + fr: self.fr.invert().unwrap(), + } + } + + fn pow(&self, n: usize) -> Self { + let mut tmp = *self; + let mut out = Self::one(); + let mut n2 = n; + + loop { + if n2 & 1 == 1 { + out = out.mul(&tmp); + } + n2 >>= 1; + if n2 == 0 { + break; + } + tmp = tmp.sqr(); + } + + out + } + + fn div(&self, b: &Self) -> Result { + if ::is_zero(b) { + return Err("Cannot divide by zero".to_string()); + } + let tmp = b.eucl_inverse(); + let out = self.mul(&tmp); + Ok(out) + } + + fn equals(&self, b: &Self) -> bool { + self.fr == b.fr + } +} + +#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)] +pub struct ZG1 { + pub proj: G1Projective, +} + +impl ZG1 { + pub const fn from_blst_p1(p1: blst_p1) -> Self { + Self { + proj: blst_p1_into_pc_g1projective(&p1), + } + } + + pub const fn to_blst_p1(&self) -> blst_p1 { + pc_g1projective_into_blst_p1(self.proj) + } + pub const fn from_g1_projective(proj: G1Projective) -> Self { + Self { proj } + } + + fn affine_to_projective(p: G1Affine) -> Self { + Self { + proj: G1Projective::from(&p), + } + } + pub fn converter(points: &[ZG1]) -> Vec { + let mut result = Vec::new(); + + for zg1 in points { + result.push(zg1.proj); + } + result + } +} + +impl From for ZG1 { + fn from(p1: blst_p1) -> Self { + let proj = blst_p1_into_pc_g1projective(&p1); + Self { proj } + } +} + +impl G1 for ZG1 { + fn identity() -> Self { + G1_IDENTITY + } + + fn generator() -> Self { + G1_GENERATOR + } + + fn negative_generator() -> Self { + G1_NEGATIVE_GENERATOR + } + + #[cfg(feature = "rand")] + fn rand() -> Self { + let mut rng = rand::thread_rng(); + Self { + proj: G1Projective::random(&mut rng), + } + } + + #[allow(clippy::bind_instead_of_map)] + fn from_bytes(bytes: &[u8]) -> Result { + bytes + .try_into() + .map_err(|_| { + format!( + "Invalid byte length. Expected {}, got {}", + BYTES_PER_G1, + bytes.len() + ) + }) + .and_then(|bytes: &[u8; BYTES_PER_G1]| { + let affine: CtOption = G1Affine::from_compressed(bytes); + match affine.into() { + Some(x) => Ok(ZG1::affine_to_projective(x)), + None => Err("Failed to deserialize G1: Affine not available".to_string()), + } + }) + } + + fn from_hex(hex: &str) -> Result { + let bytes = hex::decode(&hex[2..]).unwrap(); + Self::from_bytes(&bytes) + } + + fn to_bytes(&self) -> [u8; 48] { + let g1_affine = G1Affine::from(self.proj); + g1_affine.to_compressed() + } + //zyme + fn add_or_dbl(&mut self, b: &Self) -> Self { + Self { + proj: self.proj + b.proj, + } + } + fn is_inf(&self) -> bool { + bool::from(self.proj.is_identity()) + } + fn is_valid(&self) -> bool { + bool::from(self.proj.is_on_curve()) + } + + fn dbl(&self) -> Self { + Self { + proj: self.proj.double(), + } + } + fn add(&self, b: &Self) -> Self { + Self { + proj: self.proj + b.proj, + } + } + + fn sub(&self, b: &Self) -> Self { + Self { + proj: self.proj.sub(&b.proj), + } + } + + fn equals(&self, b: &Self) -> bool { + self.proj.eq(&b.proj) + } +} + +impl G1Mul for ZG1 { + fn mul(&self, b: &ZFr) -> Self { + Self { + proj: self.proj.mul(b.fr), + } + } + + fn g1_lincomb(points: &[Self], scalars: &[ZFr], len: usize) -> Self { + let mut out = Self::default(); + g1_linear_combination(&mut out, points, scalars, len); + out + } +} + +impl PairingVerify for ZG1 { + fn verify(a1: &ZG1, a2: &ZG2, b1: &ZG1, b2: &ZG2) -> bool { + pairings_verify(a1, a2, b1, b2) + } +} + +#[derive(Debug, Default, PartialEq, Eq, Clone)] +pub struct ZG2 { + pub proj: G2Projective, +} + +impl ZG2 { + pub const fn from_blst_p2(p2: blst::blst_p2) -> Self { + Self { + proj: blst_p2_into_pc_g2projective(&p2), + } + } + pub const fn from_g2_projective(proj: G2Projective) -> Self { + Self { proj } + } + pub const fn to_blst_p2(&self) -> blst::blst_p2 { + pc_g2projective_into_blst_p2(self.proj) + } +} + +impl G2 for ZG2 { + fn generator() -> Self { + G2_GENERATOR + } + + fn negative_generator() -> Self { + G2_NEGATIVE_GENERATOR + } + + #[allow(clippy::bind_instead_of_map)] + fn from_bytes(bytes: &[u8]) -> Result { + bytes + .try_into() + .map_err(|_| { + format!( + "Invalid byte length. Expected {}, got {}", + BYTES_PER_G2, + bytes.len() + ) + }) + .and_then(|bytes: &[u8; BYTES_PER_G2]| { + let affine = G2Affine::from_compressed(bytes).unwrap(); + Ok(ZG2::from_g2_projective(G2Projective::from(affine))) + }) + } + + fn to_bytes(&self) -> [u8; 96] { + let g2_affine = G2Affine::from(self.proj); + g2_affine.to_compressed() + } + + fn add_or_dbl(&mut self, b: &Self) -> Self { + Self { + proj: self.proj + b.proj, + } + } + + fn dbl(&self) -> Self { + Self { + proj: self.proj.double(), + } + } + + fn sub(&self, b: &Self) -> Self { + Self { + proj: self.proj - b.proj, + } + } + + fn equals(&self, b: &Self) -> bool { + self.proj.eq(&b.proj) + } +} + +impl G2Mul for ZG2 { + fn mul(&self, b: &ZFr) -> Self { + // FIXME: Is this right? + Self { + proj: self.proj.mul(b.fr), + } + } +} + +impl Default for ZFFTSettings { + fn default() -> Self { + Self { + max_width: 0, + root_of_unity: ZFr::zero(), + expanded_roots_of_unity: Vec::new(), + reverse_roots_of_unity: Vec::new(), + roots_of_unity: Vec::new(), + } + } +} + +impl FFTSettings for ZFFTSettings { + fn new(scale: usize) -> Result { + if scale >= SCALE2_ROOT_OF_UNITY.len() { + return Err(String::from( + "Scale is expected to be within root of unity matrix row size", + )); + } + + // max_width = 2 ^ max_scale + let max_width: usize = 1 << scale; + let root_of_unity = ZFr::from_u64_arr(&SCALE2_ROOT_OF_UNITY[scale]); + + // create max_width of roots & store them reversed as well + let expanded_roots_of_unity = expand_root_of_unity(&root_of_unity, max_width).unwrap(); + let mut reverse_roots_of_unity = expanded_roots_of_unity.clone(); + reverse_roots_of_unity.reverse(); + + // Permute the roots of unity + let mut roots_of_unity = expanded_roots_of_unity.clone(); + roots_of_unity.pop(); + reverse_bit_order(&mut roots_of_unity)?; + + Ok(Self { + max_width, + root_of_unity, + expanded_roots_of_unity, + reverse_roots_of_unity, + roots_of_unity, + }) + } + + fn get_max_width(&self) -> usize { + self.max_width + } + + fn get_expanded_roots_of_unity_at(&self, i: usize) -> ZFr { + self.expanded_roots_of_unity[i] + } + + fn get_expanded_roots_of_unity(&self) -> &[ZFr] { + &self.expanded_roots_of_unity + } + + fn get_reverse_roots_of_unity_at(&self, i: usize) -> ZFr { + self.reverse_roots_of_unity[i] + } + + fn get_reversed_roots_of_unity(&self) -> &[ZFr] { + &self.reverse_roots_of_unity + } + + fn get_roots_of_unity_at(&self, i: usize) -> ZFr { + self.roots_of_unity[i] + } + + fn get_roots_of_unity(&self) -> &[ZFr] { + &self.roots_of_unity + } +} + +impl KZGSettings for ZKZGSettings { + fn new( + secret_g1: &[ZG1], + secret_g2: &[ZG2], + _length: usize, + fft_settings: &ZFFTSettings, + ) -> Result { + Ok(Self { + secret_g1: secret_g1.to_vec(), + secret_g2: secret_g2.to_vec(), + fs: fft_settings.clone(), + }) + } + + fn commit_to_poly(&self, p: &PolyData) -> Result { + if p.coeffs.len() > self.secret_g1.len() { + return Err(String::from("Polynomial is longer than secret g1")); + } + + let mut out = ZG1::default(); + g1_linear_combination(&mut out, &self.secret_g1, &p.coeffs, p.coeffs.len()); + + Ok(out) + } + + fn compute_proof_single(&self, p: &PolyData, x: &ZFr) -> Result { + if p.coeffs.is_empty() { + return Err(String::from("Polynomial must not be empty")); + } + + // `-(x0^n)`, where `n` is `1` + let divisor_0 = x.negate(); + + // Calculate `q = p / (x^n - x0^n)` for our reduced case (see `compute_proof_multi` for + // generic implementation) + let mut out_coeffs = Vec::from(&p.coeffs[1..]); + for i in (1..out_coeffs.len()).rev() { + let tmp = out_coeffs[i].mul(&divisor_0); + out_coeffs[i - 1] = out_coeffs[i - 1].sub(&tmp); + } + + let q = PolyData { coeffs: out_coeffs }; + let ret = self.commit_to_poly(&q)?; + Ok(ret) + } + + fn check_proof_single(&self, com: &ZG1, proof: &ZG1, x: &ZFr, y: &ZFr) -> Result { + let x_g2 = G2_GENERATOR.mul(x); + let s_minus_x: ZG2 = self.secret_g2[1].sub(&x_g2); + let y_g1 = G1_GENERATOR.mul(y); + let commitment_minus_y: ZG1 = com.sub(&y_g1); + + Ok(pairings_verify( + &commitment_minus_y, + &G2_GENERATOR, + proof, + &s_minus_x, + )) + } + + fn compute_proof_multi(&self, p: &PolyData, x: &ZFr, n: usize) -> Result { + if p.coeffs.is_empty() { + return Err(String::from("Polynomial must not be empty")); + } + + if !n.is_power_of_two() { + return Err(String::from("n must be a power of two")); + } + + // Construct x^n - x0^n = (x - x0.w^0)(x - x0.w^1)...(x - x0.w^(n-1)) + let mut divisor = PolyData { + coeffs: Vec::with_capacity(n + 1), + }; + + // -(x0^n) + let x_pow_n = x.pow(n); + + divisor.coeffs.push(x_pow_n.negate()); + + // Zeros + for _ in 1..n { + divisor.coeffs.push(ZFr { fr: Scalar::zero() }); + } + + // x^n + divisor.coeffs.push(ZFr { fr: Scalar::one() }); + + let mut new_polina = p.clone(); + + // Calculate q = p / (x^n - x0^n) + // let q = p.div(&divisor).unwrap(); + let q = new_polina.div(&divisor)?; + let ret = self.commit_to_poly(&q)?; + Ok(ret) + } + + fn check_proof_multi( + &self, + com: &ZG1, + proof: &ZG1, + x: &ZFr, + ys: &[ZFr], + n: usize, + ) -> Result { + if !n.is_power_of_two() { + return Err(String::from("n is not a power of two")); + } + + // Interpolate at a coset. + let mut interp = PolyData { + coeffs: self.fs.fft_fr(ys, true)?, + }; + + let inv_x = x.inverse(); // Not euclidean? + let mut inv_x_pow = inv_x; + for i in 1..n { + interp.coeffs[i] = interp.coeffs[i].mul(&inv_x_pow); + inv_x_pow = inv_x_pow.mul(&inv_x); + } + + // [x^n]_2 + let x_pow = inv_x_pow.inverse(); + + let xn2 = G2_GENERATOR.mul(&x_pow); + + // [s^n - x^n]_2 + let xn_minus_yn = self.secret_g2[n].sub(&xn2); + + // [interpolation_polynomial(s)]_1 + let is1 = self.commit_to_poly(&interp).unwrap(); + + // [commitment - interpolation_polynomial(s)]_1 = [commit]_1 - [interpolation_polynomial(s)]_1 + let commit_minus_interp = com.sub(&is1); + let ret = pairings_verify(&commit_minus_interp, &G2_GENERATOR, proof, &xn_minus_yn); + + Ok(ret) + } + + fn get_expanded_roots_of_unity_at(&self, i: usize) -> ZFr { + self.fs.get_expanded_roots_of_unity_at(i) + } + + fn get_roots_of_unity_at(&self, i: usize) -> ZFr { + self.fs.get_roots_of_unity_at(i) + } + + fn get_fft_settings(&self) -> &ZFFTSettings { + &self.fs + } + + fn get_g1_secret(&self) -> &[ZG1] { + &self.secret_g1 + } + + fn get_g2_secret(&self) -> &[ZG2] { + &self.secret_g2 + } +} diff --git a/constantine/src/lib.rs b/constantine/src/lib.rs new file mode 100644 index 000000000..a59aa0b2a --- /dev/null +++ b/constantine/src/lib.rs @@ -0,0 +1,53 @@ +pub type Pairing = blst::Pairing; +pub type Fp = blst::blst_fp; +pub type Fp12 = blst::blst_fp12; +pub type Fp6 = blst::blst_fp6; +pub type Fr = blst::blst_fr; +pub type P1 = blst::blst_p1; +pub type P1Affine = blst::blst_p1_affine; +pub type P2 = blst::blst_p2; +pub type P2Affine = blst::blst_p2_affine; +pub type Scalar = blst::blst_scalar; +pub type Uniq = blst::blst_uniq; +pub mod consts; +pub mod das; +pub mod eip_4844; +pub mod fft; +pub mod fft_g1; +pub mod fk20_proofs; +pub mod kzg_proofs; +pub mod kzg_types; +mod multiscalar_mul; +pub mod poly; +pub mod recover; +pub mod utils; +pub mod zero_poly; +trait Eq { + fn equals(&self, other: &T) -> bool; +} + +trait Inverse { + fn inverse(&self) -> T; +} + +trait Zero { + fn is_zero(&self) -> bool; +} + +impl Eq for P1 { + fn equals(&self, other: &P1) -> bool { + self.x.l.eq(&other.x.l) && self.y.l.eq(&other.x.l) && self.z.l.eq(&other.x.l) + } +} + +impl Eq for Fr { + fn equals(&self, other: &Fr) -> bool { + self.l.eq(&other.l) + } +} + +impl Zero for Fr { + fn is_zero(&self) -> bool { + self.l[0] == 0 && self.l[1] == 0 && self.l[2] == 0 && self.l[3] == 0 + } +} diff --git a/constantine/src/multiscalar_mul.rs b/constantine/src/multiscalar_mul.rs new file mode 100644 index 000000000..056dcd6bd --- /dev/null +++ b/constantine/src/multiscalar_mul.rs @@ -0,0 +1,194 @@ +//! Multiscalar multiplication implementation using pippenger algorithm. +// use dusk_bytes::Serializable; + +// use alloc::vec::*; + +use crate::kzg_types::{ZFr, ZG1}; +use bls12_381::{G1Projective, Scalar}; + +pub fn divn(mut scalar: Scalar, mut n: u32) -> Scalar { + if n >= 256 { + return Scalar::from(0); + } + + while n >= 64 { + let mut t = 0; + for i in scalar.0.iter_mut().rev() { + core::mem::swap(&mut t, i); + } + n -= 64; + } + + if n > 0 { + let mut t = 0; + for i in scalar.0.iter_mut().rev() { + let t2 = *i << (64 - n); + *i >>= n; + *i |= t; + t = t2; + } + } + + scalar +} + +/// Performs a Variable Base Multiscalar Multiplication. +#[allow(clippy::needless_collect)] +pub fn msm_variable_base(points_zg1: &[ZG1], zfrscalars: &[ZFr]) -> G1Projective { + let g1_projective_vec = ZG1::converter(points_zg1); + let points = g1_projective_vec.as_slice(); + + let scalars_vec = ZFr::converter(zfrscalars); + let scalars = scalars_vec.as_slice(); + + #[cfg(feature = "parallel")] + use rayon::prelude::*; + + let c = if scalars.len() < 32 { + 3 + } else { + ln_without_floats(scalars.len()) + 2 + }; + + let num_bits = 255usize; + let fr_one = Scalar::one(); + + let zero = G1Projective::identity(); + let window_starts: Vec<_> = (0..num_bits).step_by(c).collect(); + + #[cfg(feature = "parallel")] + let window_starts_iter = window_starts.into_par_iter(); + #[cfg(not(feature = "parallel"))] + let window_starts_iter = window_starts.into_iter(); + + // Each window is of size `c`. + // We divide up the bits 0..num_bits into windows of size `c`, and + // in parallel process each such window. + let window_sums: Vec<_> = window_starts_iter + .map(|w_start| { + let mut res = zero; + // We don't need the "zero" bucket, so we only have 2^c - 1 buckets + let mut buckets = vec![zero; (1 << c) - 1]; + scalars + .iter() + .zip(points) + .filter(|(s, _)| !(*s == &Scalar::zero())) + .for_each(|(&scalar, base)| { + if scalar == fr_one { + // We only process unit scalars once in the first window. + if w_start == 0 { + res = res.add(base); + } + } else { + let mut scalar = Scalar::montgomery_reduce( + scalar.0[0], + scalar.0[1], + scalar.0[2], + scalar.0[3], + 0, + 0, + 0, + 0, + ); + + // We right-shift by w_start, thus getting rid of the + // lower bits. + scalar = divn(scalar, w_start as u32); + // We mod the remaining bits by the window size. + let scalar = scalar.0[0] % (1 << c); + + // If the scalar is non-zero, we update the corresponding + // bucket. + // (Recall that `buckets` doesn't have a zero bucket.) + if scalar != 0 { + buckets[(scalar - 1) as usize] = + buckets[(scalar - 1) as usize].add(base); + } + } + }); + + let mut running_sum = G1Projective::identity(); + for b in buckets.into_iter().rev() { + running_sum += b; + res += &running_sum; + } + + res + }) + .collect(); + + // We store the sum for the lowest window. + let lowest = *window_sums.first().unwrap(); + // We're traversing windows from high to low. + window_sums[1..] + .iter() + .rev() + .fold(zero, |mut total, sum_i| { + total += sum_i; + for _ in 0..c { + total = total.double(); + } + total + }) + + lowest +} + +fn ln_without_floats(a: usize) -> usize { + // log2(a) * ln(2) + (log2(a) * 69 / 100) as usize +} +fn log2(x: usize) -> u32 { + if x <= 1 { + return 0; + } + + let n = x.leading_zeros(); + core::mem::size_of::() as u32 * 8 - n +} + +/* + +mod tests { + #[allow(unused_imports)] + use super::*; + + #[test] + fn pippenger_test() { + // Reuse points across different tests + let mut n = 512; + let x = Scalar::from(2128506u64).invert().unwrap(); + let y = Scalar::from(4443282u64).invert().unwrap(); + let points = (0..n) + .map(|i| G1Projective::generator() * Scalar::from(1 + i as u64)) + .collect::>(); + let scalars = (0..n) + .map(|i| x + (Scalar::from(i as u64) * y)) + .collect::>(); // fast way to make ~random but deterministic scalars + let premultiplied: Vec = scalars + .iter() + .zip(points.iter()) + .map(|(sc, pt)| pt * sc) + .collect(); + while n > 0 { + let scalars = &scalars[0..n]; + let points = &points[0..n]; + let control: G1Projective = premultiplied[0..n].iter().sum(); + let subject = pippenger( + points.to_owned().into_iter(), + scalars.to_owned().into_iter(), + ); + assert_eq!(subject, control); + n = n / 2; + } + } + + #[test] + fn msm_variable_base_test() { + let points = vec![G1Affine::generator()]; + let scalars = vec![Scalar::from(100u64)]; + let premultiplied = G1Projective::generator() * Scalar::from(100u64); + let subject = msm_variable_base(&points, &scalars); + assert_eq!(subject, premultiplied); + } +} +*/ diff --git a/constantine/src/poly.rs b/constantine/src/poly.rs new file mode 100644 index 000000000..07851320e --- /dev/null +++ b/constantine/src/poly.rs @@ -0,0 +1,404 @@ +extern crate alloc; + +use alloc::string::String; +use alloc::vec; +use alloc::vec::Vec; + +use crate::consts::SCALE_FACTOR; +use crate::kzg_proofs::FFTSettings as ZFFTSettings; +use crate::kzg_types::ZFr; +use kzg::common_utils::{log2_pow2, log2_u64, next_pow_of_2}; +use kzg::{FFTFr, FFTSettings, FFTSettingsPoly, Fr, Poly}; + +#[derive(Debug, Clone, Eq, PartialEq, Default)] +pub struct PolyData { + pub coeffs: Vec, +} +impl Poly for PolyData { + fn new(size: usize) -> Self { + Self { + coeffs: vec![ZFr::default(); size], + } + } + + fn get_coeff_at(&self, i: usize) -> ZFr { + self.coeffs[i] + } + + fn set_coeff_at(&mut self, i: usize, x: &ZFr) { + self.coeffs[i] = *x + } + + fn get_coeffs(&self) -> &[ZFr] { + &self.coeffs + } + + fn len(&self) -> usize { + self.coeffs.len() + } + + fn eval(&self, x: &ZFr) -> ZFr { + if self.coeffs.is_empty() { + return ZFr::zero(); + } else if x.is_zero() { + return self.coeffs[0]; + } + + let mut ret = self.coeffs[self.coeffs.len() - 1]; + let mut i = self.coeffs.len() - 2; + loop { + let temp = ret.mul(x); + ret = temp.add(&self.coeffs[i]); + + if i == 0 { + break; + } + i -= 1; + } + + ret + } + + fn scale(&mut self) { + let scale_factor = ZFr::from_u64(SCALE_FACTOR); + let inv_factor = scale_factor.inverse(); + + let mut factor_power = ZFr::one(); + for i in 0..self.coeffs.len() { + factor_power = factor_power.mul(&inv_factor); + self.coeffs[i] = self.coeffs[i].mul(&factor_power); + } + } + + fn unscale(&mut self) { + let scale_factor = ZFr::from_u64(SCALE_FACTOR); + + let mut factor_power = ZFr::one(); + for i in 0..self.coeffs.len() { + factor_power = factor_power.mul(&scale_factor); + self.coeffs[i] = self.coeffs[i].mul(&factor_power); + } + } + + // TODO: analyze how algo works + fn inverse(&mut self, output_len: usize) -> Result { + if output_len == 0 { + return Err(String::from("Can't produce a zero-length result")); + } else if self.coeffs.is_empty() { + return Err(String::from("Can't inverse a zero-length poly")); + } else if self.coeffs[0].is_zero() { + return Err(String::from( + "First coefficient of polynomial mustn't be zero", + )); + } + + let mut ret = PolyData { + coeffs: vec![ZFr::zero(); output_len], + }; + // If the input polynomial is constant, the remainder of the series is zero + if self.coeffs.len() == 1 { + ret.coeffs[0] = self.coeffs[0].eucl_inverse(); + + return Ok(ret); + } + + let maxd = output_len - 1; + + // Max space for multiplications is (2 * length - 1) + // Don't need the following as its recalculated inside + // let scale: usize = log2_pow2(next_pow_of_2(2 * output_len - 1)); + // let fft_settings = FsFFTSettings::new(scale).unwrap(); + + // To store intermediate results + + // Base case for d == 0 + ret.coeffs[0] = self.coeffs[0].eucl_inverse(); + let mut d: usize = 0; + let mut mask: usize = 1 << log2_u64(maxd); + while mask != 0 { + d = 2 * d + usize::from((maxd & mask) != 0); + mask >>= 1; + + // b.c -> tmp0 (we're using out for c) + // tmp0.length = min_u64(d + 1, b->length + output->length - 1); + let len_temp = (d + 1).min(self.len() + output_len - 1); + let mut tmp0 = self.mul(&ret, len_temp).unwrap(); + + // 2 - b.c -> tmp0 + for i in 0..tmp0.len() { + tmp0.coeffs[i] = tmp0.coeffs[i].negate(); + } + let fr_two = Fr::from_u64(2); + tmp0.coeffs[0] = tmp0.coeffs[0].add(&fr_two); + + // c.(2 - b.c) -> tmp1; + let tmp1 = ret.mul(&tmp0, d + 1).unwrap(); + + for i in 0..tmp1.len() { + ret.coeffs[i] = tmp1.coeffs[i]; + } + } + + if d + 1 != output_len { + return Err(String::from("D + 1 must be equal to output_len")); + } + + Ok(ret) + } + + fn div(&mut self, divisor: &Self) -> Result { + if divisor.len() >= self.len() || divisor.len() < 128 { + // Tunable parameter + self.long_div(divisor) + } else { + self.fast_div(divisor) + } + } + + fn long_div(&mut self, divisor: &Self) -> Result { + if divisor.coeffs.is_empty() { + return Err(String::from("Can't divide by zero")); + } else if divisor.coeffs[divisor.coeffs.len() - 1].is_zero() { + return Err(String::from("Highest coefficient must be non-zero")); + } + + let out_length = self.poly_quotient_length(divisor); + if out_length == 0 { + return Ok(PolyData { coeffs: vec![] }); + } + + // Special case for divisor.len() == 2 + if divisor.len() == 2 { + let divisor_0 = divisor.coeffs[0]; + let divisor_1 = divisor.coeffs[1]; + + let mut out_coeffs = Vec::from(&self.coeffs[1..]); + for i in (1..out_length).rev() { + out_coeffs[i] = out_coeffs[i].div(&divisor_1).unwrap(); + + let tmp = out_coeffs[i].mul(&divisor_0); + out_coeffs[i - 1] = out_coeffs[i - 1].sub(&tmp); + } + + out_coeffs[0] = out_coeffs[0].div(&divisor_1).unwrap(); + + Ok(PolyData { coeffs: out_coeffs }) + } else { + let mut out: PolyData = PolyData { + coeffs: vec![ZFr::default(); out_length], + }; + + let mut a_pos = self.len() - 1; + let b_pos = divisor.len() - 1; + let mut diff = a_pos - b_pos; + + let mut a = self.coeffs.clone(); + + while diff > 0 { + out.coeffs[diff] = a[a_pos].div(&divisor.coeffs[b_pos]).unwrap(); + + for i in 0..(b_pos + 1) { + let tmp = out.coeffs[diff].mul(&divisor.coeffs[i]); + a[diff + i] = a[diff + i].sub(&tmp); + } + + diff -= 1; + a_pos -= 1; + } + + out.coeffs[0] = a[a_pos].div(&divisor.coeffs[b_pos]).unwrap(); + Ok(out) + } + } + + fn fast_div(&mut self, divisor: &Self) -> Result { + if divisor.coeffs.is_empty() { + return Err(String::from("Cant divide by zero")); + } else if divisor.coeffs[divisor.coeffs.len() - 1].is_zero() { + return Err(String::from("Highest coefficient must be non-zero")); + } + + let m: usize = self.len() - 1; + let n: usize = divisor.len() - 1; + + // If the divisor is larger than the dividend, the result is zero-length + if n > m { + return Ok(PolyData { coeffs: Vec::new() }); + } + + // Special case for divisor.length == 1 (it's a constant) + if divisor.len() == 1 { + let mut out = PolyData { + coeffs: vec![ZFr::zero(); self.len()], + }; + for i in 0..out.len() { + out.coeffs[i] = self.coeffs[i].div(&divisor.coeffs[0]).unwrap(); + } + return Ok(out); + } + + let mut a_flip = self.flip().unwrap(); + let mut b_flip = divisor.flip().unwrap(); + + let inv_b_flip = b_flip.inverse(m - n + 1).unwrap(); + let q_flip = a_flip.mul(&inv_b_flip, m - n + 1).unwrap(); + + let out = q_flip.flip().unwrap(); + Ok(out) + } + + fn mul_direct(&mut self, multiplier: &Self, output_len: usize) -> Result { + if self.len() == 0 || multiplier.len() == 0 { + return Ok(PolyData::new(0)); + } + + let a_degree = self.len() - 1; + let b_degree = multiplier.len() - 1; + + let mut ret = PolyData { + coeffs: vec![Fr::zero(); output_len], + }; + + // Truncate the output to the length of the output polynomial + for i in 0..(a_degree + 1) { + let mut j = 0; + while (j <= b_degree) && ((i + j) < output_len) { + let tmp = self.coeffs[i].mul(&multiplier.coeffs[j]); + let tmp = ret.coeffs[i + j].add(&tmp); + ret.coeffs[i + j] = tmp; + + j += 1; + } + } + + Ok(ret) + } +} + +impl FFTSettingsPoly for ZFFTSettings { + fn poly_mul_fft( + a: &PolyData, + b: &PolyData, + len: usize, + _fs: Option<&ZFFTSettings>, + ) -> Result { + b.mul_fft(a, len) + } +} + +impl PolyData { + pub fn _poly_norm(&self) -> Self { + let mut ret = self.clone(); + + let mut temp_len: usize = ret.coeffs.len(); + while temp_len > 0 && ret.coeffs[temp_len - 1].is_zero() { + temp_len -= 1; + } + + if temp_len == 0 { + ret.coeffs = Vec::new(); + } else { + ret.coeffs = ret.coeffs[0..temp_len].to_vec(); + } + + ret + } + + pub fn poly_quotient_length(&self, divisor: &Self) -> usize { + if self.len() >= divisor.len() { + self.len() - divisor.len() + 1 + } else { + 0 + } + } + + pub fn pad(&self, out_length: usize) -> Self { + let mut ret = Self { + coeffs: vec![ZFr::zero(); out_length], + }; + + for i in 0..self.len().min(out_length) { + ret.coeffs[i] = self.coeffs[i]; + } + + ret + } + + pub fn flip(&self) -> Result { + let mut ret = PolyData { + coeffs: vec![ZFr::default(); self.len()], + }; + for i in 0..self.len() { + ret.coeffs[i] = self.coeffs[self.coeffs.len() - i - 1] + } + + Ok(ret) + } + + pub fn mul_fft(&self, multiplier: &Self, output_len: usize) -> Result { + let length = next_pow_of_2(self.len() + multiplier.len() - 1); + + let scale = log2_pow2(length); + let fft_settings = ZFFTSettings::new(scale).unwrap(); + + let a_pad = self.pad(length); + let b_pad = multiplier.pad(length); + + let a_fft: Vec; + let b_fft: Vec; + + #[cfg(feature = "parallel")] + { + if length > 1024 { + let mut a_fft_temp = vec![]; + let mut b_fft_temp = vec![]; + + rayon::join( + || a_fft_temp = fft_settings.fft_fr(&a_pad.coeffs, false).unwrap(), + || b_fft_temp = fft_settings.fft_fr(&b_pad.coeffs, false).unwrap(), + ); + + a_fft = a_fft_temp; + b_fft = b_fft_temp; + } else { + a_fft = fft_settings.fft_fr(&a_pad.coeffs, false).unwrap(); + b_fft = fft_settings.fft_fr(&b_pad.coeffs, false).unwrap(); + } + } + + #[cfg(not(feature = "parallel"))] + { + // Convert Poly to values + a_fft = fft_settings.fft_fr(&a_pad.coeffs, false).unwrap(); + b_fft = fft_settings.fft_fr(&b_pad.coeffs, false).unwrap(); + } + + // Multiply two value ranges + let mut ab_fft = a_fft; + ab_fft.iter_mut().zip(b_fft).for_each(|(a, b)| { + *a = a.mul(&b); + }); + + // Convert value range multiplication to a resulting polynomial + let ab = fft_settings.fft_fr(&ab_fft, true).unwrap(); + drop(ab_fft); + + let mut ret = PolyData { + coeffs: vec![ZFr::zero(); output_len], + }; + + let range = ..output_len.min(length); + ret.coeffs[range].clone_from_slice(&ab[range]); + + Ok(ret) + } + + pub fn mul(&mut self, multiplier: &Self, output_len: usize) -> Result { + if self.len() < 64 || multiplier.len() < 64 || output_len < 128 { + // Tunable parameter + self.mul_direct(multiplier, output_len) + } else { + self.mul_fft(multiplier, output_len) + } + } +} diff --git a/constantine/src/recover.rs b/constantine/src/recover.rs new file mode 100644 index 000000000..370f888fe --- /dev/null +++ b/constantine/src/recover.rs @@ -0,0 +1,233 @@ +use crate::consts::SCALE_FACTOR; +use crate::kzg_proofs::FFTSettings; +use crate::kzg_types::ZFr as BlstFr; +use crate::poly::PolyData; + +use kzg::{FFTFr, Fr, Poly, PolyRecover, ZeroPoly}; + +#[cfg(feature = "parallel")] +use kzg::common_utils::next_pow_of_2; + +#[cfg(feature = "parallel")] +static mut INVERSE_FACTORS: Vec = Vec::new(); +#[cfg(feature = "parallel")] +static mut UNSCALE_FACTOR_POWERS: Vec = Vec::new(); +#[allow(clippy::needless_range_loop)] +pub fn scale_poly(p: &mut PolyData) { + let scale_factor = BlstFr::from_u64(SCALE_FACTOR); + let inv_factor = scale_factor.inverse(); + #[cfg(feature = "parallel")] + { + let optim = next_pow_of_2(p.len() - 1); + if optim <= 1024 { + unsafe { + if INVERSE_FACTORS.len() < p.len() { + if INVERSE_FACTORS.is_empty() { + INVERSE_FACTORS.push(BlstFr::one()); + } + for i in (INVERSE_FACTORS.len())..p.len() { + INVERSE_FACTORS.push(INVERSE_FACTORS[i - 1].mul(&inv_factor)); + } + } + + for i in 1..p.len() { + p.coeffs[i] = p.coeffs[i].mul(&INVERSE_FACTORS[i]); + } + } + } else { + let mut factor_power = BlstFr::one(); + for i in 1..p.len() { + factor_power = factor_power.mul(&inv_factor); + p.set_coeff_at(i, &p.get_coeff_at(i).mul(&factor_power)); + } + } + } + #[cfg(not(feature = "parallel"))] + { + let mut factor_power = BlstFr::one(); + for i in 1..p.len() { + factor_power = factor_power.mul(&inv_factor); + p.set_coeff_at(i, &p.get_coeff_at(i).mul(&factor_power)); + } + } +} + +#[allow(clippy::needless_range_loop)] +pub fn unscale_poly(p: &mut PolyData) { + let scale_factor = BlstFr::from_u64(SCALE_FACTOR); + #[cfg(feature = "parallel")] + { + unsafe { + if UNSCALE_FACTOR_POWERS.len() < p.len() { + if UNSCALE_FACTOR_POWERS.is_empty() { + UNSCALE_FACTOR_POWERS.push(BlstFr::one()); + } + for i in (UNSCALE_FACTOR_POWERS.len())..p.len() { + UNSCALE_FACTOR_POWERS.push(UNSCALE_FACTOR_POWERS[i - 1].mul(&scale_factor)); + } + } + + for i in 1..p.len() { + p.coeffs[i] = p.coeffs[i].mul(&UNSCALE_FACTOR_POWERS[i]); + } + } + } + #[cfg(not(feature = "parallel"))] + { + let mut factor_power = BlstFr::one(); + for i in 1..p.len() { + factor_power = factor_power.mul(&scale_factor); + p.set_coeff_at(i, &p.get_coeff_at(i).mul(&factor_power)); + } + } +} +impl PolyRecover for PolyData { + fn recover_poly_coeffs_from_samples( + samples: &[Option], + fs: &FFTSettings, + ) -> Result { + if !samples.len().is_power_of_two() { + return Err(String::from("samples lenght has to be power of 2")); + } + + let mut missing = Vec::new(); + + for (i, sample) in samples.iter().enumerate() { + if sample.is_none() { + missing.push(i); + } + } + + if missing.len() > samples.len() / 2 { + return Err(String::from( + "Impossible to recover, too many shards are missing", + )); + } + + // Calculate `Z_r,I` + let (zero_eval, mut zero_poly) = + fs.zero_poly_via_multiplication(samples.len(), missing.as_slice())?; + + // Check all is well + for (i, item) in zero_eval.iter().enumerate().take(samples.len()) { + if samples[i].is_none() != item.is_zero() { + return Err(String::from("sample and item are both zero")); + } + } + + // Construct E * Z_r,I: the loop makes the evaluation polynomial + + let mut poly_evaluations_with_zero = vec![BlstFr::zero(); samples.len()]; + + for i in 0..samples.len() { + if samples[i].is_none() { + poly_evaluations_with_zero[i] = BlstFr::zero(); + } else { + poly_evaluations_with_zero[i] = samples[i].unwrap().mul(&zero_eval[i]); + } + } + + // Now inverse FFT so that poly_with_zero is (E * Z_r,I)(x) = (D * Z_r,I)(x) + let mut poly_with_zero = PolyData { + coeffs: fs + .fft_fr(poly_evaluations_with_zero.as_slice(), true) + .unwrap(), + }; + + #[cfg(feature = "parallel")] + let optim = next_pow_of_2(poly_with_zero.len() - 1); + + #[cfg(feature = "parallel")] + { + if optim > 1024 { + rayon::join( + || scale_poly(&mut poly_with_zero), + || scale_poly(&mut zero_poly), + ); + } else { + scale_poly(&mut poly_with_zero); + scale_poly(&mut zero_poly); + } + } + #[cfg(not(feature = "parallel"))] + { + scale_poly(&mut poly_with_zero); + scale_poly(&mut zero_poly); + } + + // Q1 = (D * Z_r,I)(k * x) + let scaled_poly_with_zero = poly_with_zero; // Renaming + // Q2 = Z_r,I(k * x) + let scaled_zero_poly = zero_poly.coeffs; // Renaming + + let eval_scaled_poly_with_zero; + let eval_scaled_zero_poly; + + #[cfg(feature = "parallel")] + { + if optim > 1024 { + let mut eval_scaled_poly_with_zero_temp = vec![]; + let mut eval_scaled_zero_poly_temp = vec![]; + rayon::join( + || { + eval_scaled_poly_with_zero_temp = + fs.fft_fr(&scaled_poly_with_zero.coeffs, false).unwrap() + }, + || eval_scaled_zero_poly_temp = fs.fft_fr(&scaled_zero_poly, false).unwrap(), + ); + + eval_scaled_poly_with_zero = eval_scaled_poly_with_zero_temp; + eval_scaled_zero_poly = eval_scaled_zero_poly_temp; + } else { + eval_scaled_poly_with_zero = + fs.fft_fr(&scaled_poly_with_zero.coeffs, false).unwrap(); + eval_scaled_zero_poly = fs.fft_fr(&scaled_zero_poly, false).unwrap(); + } + } + #[cfg(not(feature = "parallel"))] + { + eval_scaled_poly_with_zero = fs.fft_fr(&scaled_poly_with_zero.coeffs, false).unwrap(); + eval_scaled_zero_poly = fs.fft_fr(&scaled_zero_poly, false).unwrap(); + } + + let mut eval_scaled_reconstructed_poly = eval_scaled_poly_with_zero.clone(); + for i in 0..samples.len() { + eval_scaled_reconstructed_poly[i] = eval_scaled_poly_with_zero[i] + .div(&eval_scaled_zero_poly[i]) + .unwrap(); + } + + // The result of the division is D(k * x): + let mut scaled_reconstructed_poly = PolyData { + coeffs: fs.fft_fr(&eval_scaled_reconstructed_poly, true).unwrap(), + }; + + // k * x -> x + unscale_poly(&mut scaled_reconstructed_poly); + + // Finally we have D(x) which evaluates to our original data at the powers of roots of unity + Ok(scaled_reconstructed_poly) + } + + fn recover_poly_from_samples( + samples: &[Option], + fs: &FFTSettings, + ) -> Result { + let reconstructed_poly = Self::recover_poly_coeffs_from_samples(samples, fs)?; + + // The evaluation polynomial for D(x) is the reconstructed data: + let out = PolyData { + coeffs: fs.fft_fr(&reconstructed_poly.coeffs, false).unwrap(), + }; + + // Check all is well + for (i, sample) in samples.iter().enumerate() { + if !sample.is_none() && !out.get_coeff_at(i).equals(&sample.unwrap()) { + return Err(String::from( + "sample is zero and out coeff at i is not equals to sample", + )); + } + } + Ok(out) + } +} diff --git a/constantine/src/utils.rs b/constantine/src/utils.rs new file mode 100644 index 000000000..f76d95fd2 --- /dev/null +++ b/constantine/src/utils.rs @@ -0,0 +1,58 @@ +use super::P1; +use crate::P2; +use bls12_381::{Fp as ZFp, Fp2 as ZFp2, G1Projective, G2Projective, Scalar}; +use blst::{blst_fp, blst_fp2, blst_fr, blst_p1, blst_p2}; + +#[derive(Debug, PartialEq, Eq)] +pub struct Error; + +pub const fn blst_fr_into_pc_fr(fr: blst_fr) -> Scalar { + Scalar(fr.l) +} +pub const fn pc_fr_into_blst_fr(scalar: Scalar) -> blst_fr { + blst_fr { l: scalar.0 } +} +pub const fn blst_fp2_into_pc_fq2(fp: &blst_fp2) -> ZFp2 { + let c0 = ZFp(fp.fp[0].l); + let c1 = ZFp(fp.fp[1].l); + ZFp2 { c0, c1 } +} + +pub const fn blst_p1_into_pc_g1projective(p1: &P1) -> G1Projective { + let x = ZFp(p1.x.l); + let y = ZFp(p1.y.l); + let z = ZFp(p1.z.l); + G1Projective { x, y, z } +} + +pub const fn pc_g1projective_into_blst_p1(p1: G1Projective) -> blst_p1 { + let x = blst_fp { l: p1.x.0 }; + let y = blst_fp { l: p1.y.0 }; + let z = blst_fp { l: p1.z.0 }; + + blst_p1 { x, y, z } +} + +pub const fn blst_p2_into_pc_g2projective(p2: &P2) -> G2Projective { + G2Projective { + x: blst_fp2_into_pc_fq2(&p2.x), + y: blst_fp2_into_pc_fq2(&p2.y), + z: blst_fp2_into_pc_fq2(&p2.z), + } +} + +pub const fn pc_g2projective_into_blst_p2(p2: G2Projective) -> blst_p2 { + let x = blst_fp2 { + fp: [blst_fp { l: p2.x.c0.0 }, blst_fp { l: p2.x.c1.0 }], + }; + + let y = blst_fp2 { + fp: [blst_fp { l: p2.y.c0.0 }, blst_fp { l: p2.y.c1.0 }], + }; + + let z = blst_fp2 { + fp: [blst_fp { l: p2.z.c0.0 }, blst_fp { l: p2.z.c1.0 }], + }; + + blst_p2 { x, y, z } +} diff --git a/constantine/src/zero_poly.rs b/constantine/src/zero_poly.rs new file mode 100644 index 000000000..fc6e76f64 --- /dev/null +++ b/constantine/src/zero_poly.rs @@ -0,0 +1,197 @@ +use super::kzg_proofs::FFTSettings; +use crate::kzg_types::ZFr as BlstFr; +use crate::poly::PolyData; + +use kzg::common_utils::next_pow_of_2; +use kzg::{FFTFr, Fr, ZeroPoly}; +use std::cmp::{min, Ordering}; + +pub(crate) fn pad_poly(poly: &PolyData, new_length: usize) -> Result, String> { + if new_length <= poly.coeffs.len() { + return Ok(poly.coeffs.clone()); + } + + let mut out = poly.coeffs.to_vec(); + + for _i in poly.coeffs.len()..new_length { + out.push(BlstFr::zero()) + } + + Ok(out) +} +impl ZeroPoly for FFTSettings { + #[allow(clippy::needless_range_loop)] + fn do_zero_poly_mul_partial( + &self, + indices: &[usize], + stride: usize, + ) -> Result { + if indices.is_empty() { + // == 0 + return Err(String::from("index array length mustnt be zero")); + } + let mut poly = PolyData { + coeffs: vec![BlstFr::one(); indices.len() + 1], + }; + poly.coeffs[0] = (self.expanded_roots_of_unity[indices[0] * stride]).negate(); + + for i in 1..indices.len() { + let neg_di = (self.expanded_roots_of_unity[indices[i] * stride]).negate(); + poly.coeffs[i] = neg_di; + + poly.coeffs[i] = poly.coeffs[i].add(&poly.coeffs[i - 1]); + + let mut j = i - 1; + while j > 0 { + poly.coeffs[j] = poly.coeffs[j].mul(&neg_di); + poly.coeffs[j] = poly.coeffs[j].add(&poly.coeffs[j - 1]); + j -= 1; + } + + poly.coeffs[0] = poly.coeffs[0].mul(&neg_di); + } + + Ok(poly) + } + #[allow(clippy::needless_range_loop)] + fn reduce_partials(&self, len_out: usize, partials: &[PolyData]) -> Result { + let mut out_degree: usize = 0; + for partial in partials { + out_degree += partial.coeffs.len() - 1; + } + + if out_degree + 1 > len_out { + return Err(String::from("Expected domain size to be a power of 2")); + } + + let mut p_partial = pad_poly(&partials[0], len_out).unwrap(); + let mut mul_eval_ps = self.fft_fr(&p_partial, false).unwrap(); + + for partial in partials.iter().skip(1) { + p_partial = pad_poly(partial, len_out)?; + + let p_eval = self.fft_fr(&p_partial, false).unwrap(); + for j in 0..len_out { + mul_eval_ps[j].fr *= p_eval[j].fr; + } + } + + let coeffs = self.fft_fr(&mul_eval_ps, true)?; + + let out = PolyData { + coeffs: coeffs[..(out_degree + 1)].to_vec(), + }; + + Ok(out) + } + #[allow(clippy::comparison_chain)] + fn zero_poly_via_multiplication( + &self, + length: usize, + missing_indices: &[usize], + ) -> Result<(Vec, PolyData), String> { + let zero_eval: Vec; + let mut zero_poly: PolyData; + + if missing_indices.is_empty() { + zero_eval = Vec::new(); + zero_poly = PolyData { coeffs: Vec::new() }; + return Ok((zero_eval, zero_poly)); + } + + if missing_indices.len() >= length { + return Err(String::from("Missing idxs greater than domain size")); + } else if length > self.max_width { + return Err(String::from( + "Domain size greater than fft_settings.max_width", + )); + } else if !length.is_power_of_two() { + return Err(String::from("Domain size must be a power of 2")); + } + + let degree_of_partial = 256; + let missing_per_partial = degree_of_partial - 1; + let domain_stride = self.max_width / length; + let mut partial_count = + (missing_per_partial + missing_indices.len() - 1) / missing_per_partial; + let domain_ceiling = min(next_pow_of_2(partial_count * degree_of_partial), length); + + if missing_indices.len() <= missing_per_partial { + zero_poly = self.do_zero_poly_mul_partial(missing_indices, domain_stride)?; + } else { + let mut work = vec![BlstFr::zero(); next_pow_of_2(partial_count * degree_of_partial)]; + + let mut partial_lens = Vec::new(); + + let mut offset = 0; + let mut out_offset = 0; + let max = missing_indices.len(); + + for _i in 0..partial_count { + let end = min(offset + missing_per_partial, max); + + let mut partial = + self.do_zero_poly_mul_partial(&missing_indices[offset..end], domain_stride)?; + partial.coeffs = pad_poly(&partial, degree_of_partial)?; + work.splice( + out_offset..(out_offset + degree_of_partial), + partial.coeffs.to_vec(), + ); + partial_lens.push(degree_of_partial); + + offset += missing_per_partial; + out_offset += degree_of_partial; + } + + partial_lens[partial_count - 1] = + 1 + missing_indices.len() - (partial_count - 1) * missing_per_partial; + + let reduction_factor = 4; + while partial_count > 1 { + let reduced_count = 1 + (partial_count - 1) / reduction_factor; + let partial_size = next_pow_of_2(partial_lens[0]); + + for i in 0..reduced_count { + let start = i * reduction_factor; + let out_end = min((start + reduction_factor) * partial_size, domain_ceiling); + let reduced_len = min(out_end - start * partial_size, length); + let partials_num = min(reduction_factor, partial_count - start); + + let mut partial_vec = Vec::new(); + for j in 0..partials_num { + let k = (start + j) * partial_size; + partial_vec.push(PolyData { + coeffs: work[k..(k + partial_lens[i + j])].to_vec(), + }); + } + + if partials_num > 1 { + let mut reduced_poly = self.reduce_partials(reduced_len, &partial_vec)?; + partial_lens[i] = reduced_poly.coeffs.len(); + reduced_poly.coeffs = pad_poly(&reduced_poly, partial_size * partials_num)?; + work.splice( + (start * partial_size) + ..(start * partial_size + reduced_poly.coeffs.len()), + reduced_poly.coeffs, + ); + } else { + partial_lens[i] = partial_lens[start]; + } + } + + partial_count = reduced_count; + } + + zero_poly = PolyData { coeffs: work }; + } + + match zero_poly.coeffs.len().cmp(&length) { + Ordering::Less => zero_poly.coeffs = pad_poly(&zero_poly, length)?, + Ordering::Greater => zero_poly.coeffs = zero_poly.coeffs[..length].to_vec(), + Ordering::Equal => {} + } + + zero_eval = self.fft_fr(&zero_poly.coeffs, false)?; + Ok((zero_eval, zero_poly)) + } +} diff --git a/constantine/tests/bls12_381.rs b/constantine/tests/bls12_381.rs new file mode 100644 index 000000000..a123f6628 --- /dev/null +++ b/constantine/tests/bls12_381.rs @@ -0,0 +1,114 @@ +#[cfg(test)] +mod tests { + use kzg::common_utils::log_2_byte; + use kzg_bench::tests::bls12_381::*; + use rust_kzg_zkcrypto::fft_g1::g1_linear_combination; + use rust_kzg_zkcrypto::kzg_proofs::pairings_verify; + use rust_kzg_zkcrypto::kzg_types::{ZFr, ZG1, ZG2}; + + #[test] + pub fn log_2_byte_works_() { + log_2_byte_works(&log_2_byte); + } + + #[test] + pub fn fr_is_zero_works_() { + fr_is_zero_works::(); + } + + #[test] + pub fn fr_is_one_works_() { + fr_is_one_works::(); + } + + #[test] + pub fn fr_from_uint64_works_() { + fr_from_uint64_works::(); + } + + #[test] + pub fn fr_equal_works_() { + fr_equal_works::(); + } + + #[test] + pub fn fr_negate_works_() { + fr_negate_works::(); + } + + #[test] + pub fn fr_pow_works_() { + fr_pow_works::(); + } + + #[test] + pub fn fr_div_works_() { + fr_div_works::(); + } + + #[test] + #[should_panic] + pub fn fr_div_by_zero_() { + fr_div_by_zero::(); + } + + #[test] + pub fn fr_uint64s_roundtrip_() { + fr_uint64s_roundtrip::(); + } + + #[test] + pub fn p1_mul_works_() { + p1_mul_works::(); + } + + #[test] + pub fn p1_sub_works_() { + p1_sub_works::(); + } + + #[test] + pub fn p2_add_or_dbl_works_() { + p2_add_or_dbl_works::(); + } + + #[test] + pub fn p2_mul_works_() { + p2_mul_works::(); + } + + #[test] + pub fn p2_sub_works_() { + p2_sub_works::(); + } + + #[test] + pub fn g1_identity_is_infinity_() { + g1_identity_is_infinity::(); + } + + #[test] + pub fn g1_identity_is_identity_() { + g1_identity_is_identity::(); + } + + #[test] + pub fn g1_make_linear_combination_() { + g1_make_linear_combination::(&g1_linear_combination); + } + + #[test] + pub fn g1_random_linear_combination_() { + g1_random_linear_combination::(&g1_linear_combination); + } + + #[test] + pub fn pairings_work_() { + pairings_work::(&pairings_verify); + } + + #[test] + pub fn fr_is_null_works_() { + fr_is_null_works::(); + } +} diff --git a/constantine/tests/consts.rs b/constantine/tests/consts.rs new file mode 100644 index 000000000..88966e4c2 --- /dev/null +++ b/constantine/tests/consts.rs @@ -0,0 +1,36 @@ +#[cfg(test)] +mod tests { + use kzg_bench::tests::consts::{ + expand_roots_is_plausible, new_fft_settings_is_plausible, roots_of_unity_are_plausible, + roots_of_unity_is_the_expected_size, roots_of_unity_out_of_bounds_fails, + }; + use rust_kzg_zkcrypto::consts::SCALE2_ROOT_OF_UNITY; + use rust_kzg_zkcrypto::kzg_proofs::expand_root_of_unity; + use rust_kzg_zkcrypto::kzg_proofs::FFTSettings; + use rust_kzg_zkcrypto::kzg_types::ZFr; + + #[test] + fn roots_of_unity_out_of_bounds_fails_() { + roots_of_unity_out_of_bounds_fails::(); + } + + #[test] + fn roots_of_unity_are_plausible_() { + roots_of_unity_are_plausible::(&SCALE2_ROOT_OF_UNITY); + } + + #[test] + fn expand_roots_is_plausible_() { + expand_roots_is_plausible::(&SCALE2_ROOT_OF_UNITY, &expand_root_of_unity); + } + + #[test] + fn new_fft_settings_is_plausible_() { + new_fft_settings_is_plausible::(); + } + + #[test] + fn roots_of_unity_is_the_expected_size_() { + roots_of_unity_is_the_expected_size(&SCALE2_ROOT_OF_UNITY); + } +} diff --git a/constantine/tests/das.rs b/constantine/tests/das.rs new file mode 100644 index 000000000..0459a4f16 --- /dev/null +++ b/constantine/tests/das.rs @@ -0,0 +1,16 @@ +#[cfg(test)] +mod tests { + use kzg_bench::tests::das::{das_extension_test_known, das_extension_test_random}; + use rust_kzg_zkcrypto::kzg_proofs::FFTSettings; + use rust_kzg_zkcrypto::kzg_types::ZFr; + + #[test] + fn das_extension_test_known_() { + das_extension_test_known::(); + } + + #[test] + fn das_extension_test_random_() { + das_extension_test_random::(); + } +} diff --git a/constantine/tests/eip_4844.rs b/constantine/tests/eip_4844.rs new file mode 100644 index 000000000..333c9d9a7 --- /dev/null +++ b/constantine/tests/eip_4844.rs @@ -0,0 +1,247 @@ +#[cfg(test)] +mod tests { + use kzg::eip_4844::{ + blob_to_kzg_commitment_rust, blob_to_polynomial, bytes_to_blob, + compute_blob_kzg_proof_rust, compute_kzg_proof_rust, compute_powers, + evaluate_polynomial_in_evaluation_form, verify_blob_kzg_proof_batch_rust, + verify_blob_kzg_proof_rust, verify_kzg_proof_rust, + }; + use kzg::Fr; + use kzg_bench::tests::eip_4844::{ + blob_to_kzg_commitment_test, bytes_to_bls_field_test, + compute_and_verify_blob_kzg_proof_fails_with_incorrect_proof_test, + compute_and_verify_blob_kzg_proof_test, + compute_and_verify_kzg_proof_fails_with_incorrect_proof_test, + compute_and_verify_kzg_proof_round_trip_test, compute_kzg_proof_test, compute_powers_test, + verify_kzg_proof_batch_fails_with_incorrect_proof_test, verify_kzg_proof_batch_test, + }; + #[cfg(not(feature = "minimal-spec"))] + use kzg_bench::tests::eip_4844::{ + compute_and_verify_kzg_proof_within_domain_test, test_vectors_blob_to_kzg_commitment, + test_vectors_compute_blob_kzg_proof, test_vectors_compute_kzg_proof, + test_vectors_verify_blob_kzg_proof, test_vectors_verify_blob_kzg_proof_batch, + test_vectors_verify_kzg_proof, + }; + use rust_kzg_zkcrypto::consts::SCALE2_ROOT_OF_UNITY; + use rust_kzg_zkcrypto::eip_4844::load_trusted_setup_filename_rust; + use rust_kzg_zkcrypto::kzg_proofs::{expand_root_of_unity, FFTSettings, KZGSettings}; + use rust_kzg_zkcrypto::kzg_types::{ZFr, ZG1, ZG2}; + use rust_kzg_zkcrypto::poly::PolyData; + + #[test] + pub fn bytes_to_bls_field_test_() { + bytes_to_bls_field_test::(); + } + + #[test] + pub fn compute_powers_test_() { + compute_powers_test::(&compute_powers); + } + + #[test] + pub fn blob_to_kzg_commitment_test_() { + blob_to_kzg_commitment_test::( + &load_trusted_setup_filename_rust, + &blob_to_kzg_commitment_rust, + ); + } + + #[test] + pub fn compute_kzg_proof_test_() { + compute_kzg_proof_test::( + &load_trusted_setup_filename_rust, + &compute_kzg_proof_rust, + &blob_to_polynomial, + &evaluate_polynomial_in_evaluation_form, + ); + } + + #[test] + pub fn compute_and_verify_kzg_proof_round_trip_test_() { + compute_and_verify_kzg_proof_round_trip_test::< + ZFr, + ZG1, + ZG2, + PolyData, + FFTSettings, + KZGSettings, + >( + &load_trusted_setup_filename_rust, + &blob_to_kzg_commitment_rust, + &bytes_to_blob, + &compute_kzg_proof_rust, + &blob_to_polynomial, + &evaluate_polynomial_in_evaluation_form, + &verify_kzg_proof_rust, + ); + } + + #[cfg(not(feature = "minimal-spec"))] + #[test] + pub fn compute_and_verify_kzg_proof_within_domain_test_() { + compute_and_verify_kzg_proof_within_domain_test::< + ZFr, + ZG1, + ZG2, + PolyData, + FFTSettings, + KZGSettings, + >( + &load_trusted_setup_filename_rust, + &blob_to_kzg_commitment_rust, + &bytes_to_blob, + &compute_kzg_proof_rust, + &blob_to_polynomial, + &evaluate_polynomial_in_evaluation_form, + &verify_kzg_proof_rust, + ); + } + + #[test] + pub fn compute_and_verify_kzg_proof_fails_with_incorrect_proof_test_() { + compute_and_verify_kzg_proof_fails_with_incorrect_proof_test::< + ZFr, + ZG1, + ZG2, + PolyData, + FFTSettings, + KZGSettings, + >( + &load_trusted_setup_filename_rust, + &blob_to_kzg_commitment_rust, + &bytes_to_blob, + &compute_kzg_proof_rust, + &blob_to_polynomial, + &evaluate_polynomial_in_evaluation_form, + &verify_kzg_proof_rust, + ); + } + + #[test] + pub fn compute_and_verify_blob_kzg_proof_test_() { + compute_and_verify_blob_kzg_proof_test::( + &load_trusted_setup_filename_rust, + &blob_to_kzg_commitment_rust, + &bytes_to_blob, + &compute_blob_kzg_proof_rust, + &verify_blob_kzg_proof_rust, + ); + } + + #[test] + pub fn compute_and_verify_blob_kzg_proof_fails_with_incorrect_proof_test_() { + compute_and_verify_blob_kzg_proof_fails_with_incorrect_proof_test::< + ZFr, + ZG1, + ZG2, + PolyData, + FFTSettings, + KZGSettings, + >( + &load_trusted_setup_filename_rust, + &blob_to_kzg_commitment_rust, + &bytes_to_blob, + &compute_blob_kzg_proof_rust, + &verify_blob_kzg_proof_rust, + ); + } + + #[test] + pub fn verify_kzg_proof_batch_test_() { + verify_kzg_proof_batch_test::( + &load_trusted_setup_filename_rust, + &blob_to_kzg_commitment_rust, + &bytes_to_blob, + &compute_blob_kzg_proof_rust, + &verify_blob_kzg_proof_batch_rust, + ); + } + + #[test] + pub fn verify_kzg_proof_batch_fails_with_incorrect_proof_test_() { + verify_kzg_proof_batch_fails_with_incorrect_proof_test::< + ZFr, + ZG1, + ZG2, + PolyData, + FFTSettings, + KZGSettings, + >( + &load_trusted_setup_filename_rust, + &blob_to_kzg_commitment_rust, + &bytes_to_blob, + &compute_blob_kzg_proof_rust, + &verify_blob_kzg_proof_batch_rust, + ); + } + + #[cfg(not(feature = "minimal-spec"))] + #[test] + pub fn test_vectors_blob_to_kzg_commitment_() { + test_vectors_blob_to_kzg_commitment::( + &load_trusted_setup_filename_rust, + &blob_to_kzg_commitment_rust, + &bytes_to_blob, + ); + } + + #[cfg(not(feature = "minimal-spec"))] + #[test] + pub fn test_vectors_compute_kzg_proof_() { + test_vectors_compute_kzg_proof::( + &load_trusted_setup_filename_rust, + &compute_kzg_proof_rust, + &bytes_to_blob, + ); + } + + #[cfg(not(feature = "minimal-spec"))] + #[test] + pub fn test_vectors_compute_blob_kzg_proof_() { + test_vectors_compute_blob_kzg_proof::( + &load_trusted_setup_filename_rust, + &bytes_to_blob, + &compute_blob_kzg_proof_rust, + ); + } + + #[cfg(not(feature = "minimal-spec"))] + #[test] + pub fn test_vectors_verify_kzg_proof_() { + test_vectors_verify_kzg_proof::( + &load_trusted_setup_filename_rust, + &verify_kzg_proof_rust, + ); + } + + #[cfg(not(feature = "minimal-spec"))] + #[test] + pub fn test_vectors_verify_blob_kzg_proof_() { + test_vectors_verify_blob_kzg_proof::( + &load_trusted_setup_filename_rust, + &bytes_to_blob, + &verify_blob_kzg_proof_rust, + ); + } + #[cfg(not(feature = "minimal-spec"))] + #[test] + pub fn test_vectors_verify_blob_kzg_proof_batch_() { + test_vectors_verify_blob_kzg_proof_batch::( + &load_trusted_setup_filename_rust, + &bytes_to_blob, + &verify_blob_kzg_proof_batch_rust, + ); + } + + #[test] + pub fn expand_root_of_unity_too_long() { + let out = expand_root_of_unity(&ZFr::from_u64_arr(&SCALE2_ROOT_OF_UNITY[1]), 1); + assert!(out.is_err()); + } + + #[test] + pub fn expand_root_of_unity_too_short() { + let out = expand_root_of_unity(&ZFr::from_u64_arr(&SCALE2_ROOT_OF_UNITY[1]), 3); + assert!(out.is_err()); + } +} diff --git a/constantine/tests/fft_fr.rs b/constantine/tests/fft_fr.rs new file mode 100644 index 000000000..78228eefa --- /dev/null +++ b/constantine/tests/fft_fr.rs @@ -0,0 +1,27 @@ +#[cfg(test)] +mod tests { + use kzg_bench::tests::fft_fr::{compare_sft_fft, inverse_fft, roundtrip_fft, stride_fft}; + use rust_kzg_zkcrypto::fft::{fft_fr_fast, fft_fr_slow}; + use rust_kzg_zkcrypto::kzg_proofs::FFTSettings; + use rust_kzg_zkcrypto::kzg_types::ZFr; + + #[test] + fn compare_sft_fft_() { + compare_sft_fft::(&fft_fr_slow, &fft_fr_fast); + } + + #[test] + fn roundtrip_fft_() { + roundtrip_fft::(); + } + + #[test] + fn inverse_fft_() { + inverse_fft::(); + } + + #[test] + fn stride_fft_() { + stride_fft::(); + } +} diff --git a/constantine/tests/fft_g1.rs b/constantine/tests/fft_g1.rs new file mode 100644 index 000000000..6d2035557 --- /dev/null +++ b/constantine/tests/fft_g1.rs @@ -0,0 +1,22 @@ +#[cfg(test)] +mod tests { + use kzg_bench::tests::fft_g1::{compare_sft_fft, roundtrip_fft, stride_fft}; + use rust_kzg_zkcrypto::fft_g1::{fft_g1_fast, fft_g1_slow, make_data}; + use rust_kzg_zkcrypto::kzg_proofs::FFTSettings; + use rust_kzg_zkcrypto::kzg_types::{ZFr, ZG1}; + + #[test] + fn roundtrip_fft_() { + roundtrip_fft::(&make_data); + } + + #[test] + fn stride_fft_() { + stride_fft::(&make_data); + } + + #[test] + fn compare_sft_fft_() { + compare_sft_fft::(&fft_g1_slow, &fft_g1_fast, &make_data); + } +} diff --git a/constantine/tests/fk20_proofs.rs b/constantine/tests/fk20_proofs.rs new file mode 100644 index 000000000..1c9e1993a --- /dev/null +++ b/constantine/tests/fk20_proofs.rs @@ -0,0 +1,82 @@ +#[cfg(test)] +mod tests { + use kzg_bench::tests::fk20_proofs::*; + + use rust_kzg_zkcrypto::fk20_proofs::{KzgFK20MultiSettings, KzgFK20SingleSettings}; + use rust_kzg_zkcrypto::kzg_proofs::{generate_trusted_setup, FFTSettings, KZGSettings}; + use rust_kzg_zkcrypto::kzg_types::ZFr as BlstFr; + use rust_kzg_zkcrypto::kzg_types::{ZG1, ZG2}; + use rust_kzg_zkcrypto::poly::PolyData; + + #[test] + fn test_fk_single() { + fk_single::( + &generate_trusted_setup, + ); + } + + #[test] + fn test_fk_single_strided() { + fk_single_strided::< + BlstFr, + ZG1, + ZG2, + PolyData, + FFTSettings, + KZGSettings, + KzgFK20SingleSettings, + >(&generate_trusted_setup); + } + + #[test] + fn test_fk_multi_settings() { + fk_multi_settings::< + BlstFr, + ZG1, + ZG2, + PolyData, + FFTSettings, + KZGSettings, + KzgFK20MultiSettings, + >(&generate_trusted_setup); + } + + #[test] + fn test_fk_multi_chunk_len_1_512() { + fk_multi_chunk_len_1_512::< + BlstFr, + ZG1, + ZG2, + PolyData, + FFTSettings, + KZGSettings, + KzgFK20MultiSettings, + >(&generate_trusted_setup); + } + + #[test] + fn test_fk_multi_chunk_len_16_512() { + fk_multi_chunk_len_16_512::< + BlstFr, + ZG1, + ZG2, + PolyData, + FFTSettings, + KZGSettings, + KzgFK20MultiSettings, + >(&generate_trusted_setup); + } + + #[test] + fn test_fk_multi_chunk_len_16_16() { + fk_multi_chunk_len_16_16::< + BlstFr, + ZG1, + ZG2, + PolyData, + FFTSettings, + KZGSettings, + KzgFK20MultiSettings, + >(&generate_trusted_setup); + } +} diff --git a/constantine/tests/kzg_proofs.rs b/constantine/tests/kzg_proofs.rs new file mode 100644 index 000000000..6c3abf607 --- /dev/null +++ b/constantine/tests/kzg_proofs.rs @@ -0,0 +1,31 @@ +#[cfg(test)] +mod tests { + use kzg_bench::tests::kzg_proofs::{ + commit_to_nil_poly, commit_to_too_long_poly_returns_err, proof_multi, proof_single, + }; + use rust_kzg_zkcrypto::kzg_proofs::{generate_trusted_setup, FFTSettings, KZGSettings}; + use rust_kzg_zkcrypto::kzg_types::{ZFr, ZG1, ZG2}; + use rust_kzg_zkcrypto::poly::PolyData; + + #[test] + fn proof_single_() { + proof_single::(&generate_trusted_setup); + } + #[test] + fn commit_to_nil_poly_() { + commit_to_nil_poly::( + &generate_trusted_setup, + ); + } + #[test] + fn commit_to_too_long_poly_() { + commit_to_too_long_poly_returns_err::( + &generate_trusted_setup, + ); + } + + #[test] + fn proof_multi_() { + proof_multi::(&generate_trusted_setup); + } +} diff --git a/constantine/tests/poly.rs b/constantine/tests/poly.rs new file mode 100644 index 000000000..0668ce5f1 --- /dev/null +++ b/constantine/tests/poly.rs @@ -0,0 +1,82 @@ +#[cfg(test)] +mod tests { + use kzg_bench::tests::poly::{ + create_poly_of_length_ten, poly_div_by_zero, poly_div_fast_test, poly_div_long_test, + poly_div_random, poly_eval_0_check, poly_eval_check, poly_eval_nil_check, + poly_inverse_simple_0, poly_inverse_simple_1, poly_mul_direct_test, poly_mul_fft_test, + poly_mul_random, poly_test_div, + }; + use rust_kzg_zkcrypto::kzg_proofs::FFTSettings; + use rust_kzg_zkcrypto::kzg_types::ZFr; + use rust_kzg_zkcrypto::poly::PolyData; + + #[test] + fn create_poly_of_length_ten_() { + create_poly_of_length_ten::(); + } + + #[test] + fn poly_eval_check_() { + poly_eval_check::(); + } + + #[test] + fn poly_eval_0_check_() { + poly_eval_0_check::(); + } + + #[test] + fn poly_eval_nil_check_() { + poly_eval_nil_check::(); + } + + #[test] + fn poly_inverse_simple_0_() { + poly_inverse_simple_0::(); + } + + #[test] + fn poly_inverse_simple_1_() { + poly_inverse_simple_1::(); + } + + #[test] + fn poly_test_div_() { + poly_test_div::(); + } + + #[test] + fn poly_div_by_zero_() { + poly_div_by_zero::(); + } + + #[test] + fn poly_mul_direct_test_() { + poly_mul_direct_test::(); + } + + #[test] + fn poly_mul_fft_test_() { + poly_mul_fft_test::(); + } + + #[test] + fn poly_mul_random_() { + poly_mul_random::(); + } + + #[test] + fn poly_div_random_() { + poly_div_random::(); + } + + #[test] + fn poly_div_long_test_() { + poly_div_long_test::() + } + + #[test] + fn poly_div_fast_test_() { + poly_div_fast_test::() + } +} diff --git a/constantine/tests/recover.rs b/constantine/tests/recover.rs new file mode 100644 index 000000000..865849ea9 --- /dev/null +++ b/constantine/tests/recover.rs @@ -0,0 +1,23 @@ +#[cfg(test)] +mod recover_tests { + use kzg_bench::tests::recover::*; + use rust_kzg_zkcrypto::kzg_proofs::FFTSettings; + use rust_kzg_zkcrypto::kzg_types::ZFr as Fr; + use rust_kzg_zkcrypto::poly::PolyData; + + #[test] + fn recover_simple_() { + recover_simple::(); + } + + //Could be not working because of zero poly. + #[test] + fn recover_random_() { + recover_random::(); + } + + #[test] + fn more_than_half_missing_() { + more_than_half_missing::(); + } +} diff --git a/constantine/tests/zero_poly.rs b/constantine/tests/zero_poly.rs new file mode 100644 index 000000000..cab56ecf5 --- /dev/null +++ b/constantine/tests/zero_poly.rs @@ -0,0 +1,45 @@ +#[cfg(test)] +mod tests { + use kzg_bench::tests::zero_poly::{ + check_test_data, reduce_partials_random, test_reduce_partials, zero_poly_252, + zero_poly_all_but_one, zero_poly_known, zero_poly_random, + }; + use rust_kzg_zkcrypto::kzg_proofs::FFTSettings; + use rust_kzg_zkcrypto::kzg_types::ZFr; + use rust_kzg_zkcrypto::poly::PolyData; + + #[test] + fn test_reduce_partials_() { + test_reduce_partials::(); + } + + #[test] + fn reduce_partials_random_() { + reduce_partials_random::(); + } + + #[test] + fn check_test_data_() { + check_test_data::(); + } + + #[test] + fn zero_poly_known_() { + zero_poly_known::(); + } + + #[test] + fn zero_poly_random_() { + zero_poly_random::(); + } + + #[test] + fn zero_poly_all_but_one_() { + zero_poly_all_but_one::(); + } + + #[test] + fn zero_poly_252_() { + zero_poly_252::(); + } +} From acd3337aa25892fc615d64542c93f98427a59611 Mon Sep 17 00:00:00 2001 From: Armantidas Date: Sat, 16 Dec 2023 22:47:45 +0200 Subject: [PATCH 02/17] Partial constantine ecc backend --- Cargo.lock | 9 +- constantine/.gitignore | 1 + constantine/Cargo.toml | 37 +- constantine/README.md | 46 + constantine/benches/das.rs | 6 +- constantine/benches/eip_4844.rs | 15 +- constantine/benches/fft.rs | 9 +- constantine/benches/fk_20.rs | 18 +- constantine/benches/kzg.rs | 19 +- constantine/benches/lincomb.rs | 7 +- constantine/benches/poly.rs | 6 +- constantine/benches/recover.rs | 9 +- constantine/benches/zero_poly.rs | 7 +- constantine/bls12_381/Cargo.toml | 73 - constantine/bls12_381/benches/groups.rs | 170 -- .../bls12_381/benches/hash_to_curve.rs | 68 - constantine/bls12_381/src/fp.rs | 990 -------- constantine/bls12_381/src/fp12.rs | 659 ----- constantine/bls12_381/src/fp2.rs | 898 ------- constantine/bls12_381/src/fp6.rs | 571 ----- constantine/bls12_381/src/g1.rs | 1798 -------------- constantine/bls12_381/src/g2.rs | 2180 ----------------- .../bls12_381/src/hash_to_curve/chain.rs | 920 ------- .../bls12_381/src/hash_to_curve/expand_msg.rs | 1282 ---------- .../bls12_381/src/hash_to_curve/map_g1.rs | 961 -------- .../bls12_381/src/hash_to_curve/map_g2.rs | 900 ------- .../bls12_381/src/hash_to_curve/map_scalar.rs | 39 - .../bls12_381/src/hash_to_curve/mod.rs | 113 - constantine/bls12_381/src/lib.rs | 97 - constantine/bls12_381/src/notes/design.rs | 108 - .../bls12_381/src/notes/serialization.rs | 29 - constantine/bls12_381/src/pairings.rs | 970 -------- constantine/bls12_381/src/scalar.rs | 1278 ---------- .../g1_compressed_valid_test_vectors.dat | Bin 48000 -> 0 bytes .../g1_uncompressed_valid_test_vectors.dat | Bin 96000 -> 0 bytes .../g2_compressed_valid_test_vectors.dat | Bin 96000 -> 0 bytes .../g2_uncompressed_valid_test_vectors.dat | Bin 192000 -> 0 bytes constantine/bls12_381/src/tests/mod.rs | 231 -- constantine/bls12_381/src/util.rs | 174 -- constantine/csharp.patch | 2 +- constantine/go.patch | 2 +- constantine/java.patch | 2 +- constantine/nodejs.patch | 2 +- constantine/python.patch | 4 +- constantine/rust.patch | 2 +- constantine/src/consts.rs | 366 +-- constantine/src/das.rs | 87 - constantine/src/data_availability_sampling.rs | 102 + constantine/src/eip_4844.rs | 204 +- constantine/src/fft.rs | 106 - constantine/src/fft_fr.rs | 145 ++ constantine/src/fft_g1.rs | 152 +- constantine/src/fk20_proofs.rs | 355 +-- constantine/src/kzg_proofs.rs | 174 +- constantine/src/kzg_types.rs | 745 ------ constantine/src/lib.rs | 52 +- constantine/src/multiscalar_mul.rs | 194 -- constantine/src/recover.rs | 233 -- constantine/src/recovery.rs | 195 ++ constantine/src/types/fft_settings.rs | 106 + constantine/src/types/fk20_multi_settings.rs | 163 ++ constantine/src/types/fk20_single_settings.rs | 99 + constantine/src/types/fr.rs | 253 ++ constantine/src/types/g1.rs | 171 ++ constantine/src/types/g2.rs | 121 + constantine/src/types/kzg_settings.rs | 191 ++ constantine/src/types/mod.rs | 8 + constantine/src/{ => types}/poly.rs | 86 +- constantine/src/utils.rs | 67 +- constantine/src/zero_poly.rs | 373 ++- constantine/tests/bls12_381.rs | 101 +- constantine/tests/c_bindings.rs | 80 + constantine/tests/consts.rs | 33 +- constantine/tests/das.rs | 8 +- constantine/tests/eip_4844.rs | 199 +- constantine/tests/fft_fr.rs | 14 +- constantine/tests/fft_g1.rs | 30 +- constantine/tests/fk20_proofs.rs | 87 +- constantine/tests/kzg_proofs.rs | 91 +- constantine/tests/local_tests/local_consts.rs | 16 + constantine/tests/local_tests/local_poly.rs | 371 +++ .../tests/local_tests/local_recovery.rs | 158 ++ constantine/tests/local_tests/mod.rs | 3 + constantine/tests/mod.rs | 1 + constantine/tests/poly.rs | 107 +- constantine/tests/recover.rs | 23 - constantine/tests/recovery.rs | 29 + constantine/tests/zero_poly.rs | 20 +- 88 files changed, 3684 insertions(+), 17147 deletions(-) create mode 100644 constantine/.gitignore create mode 100644 constantine/README.md delete mode 100644 constantine/bls12_381/Cargo.toml delete mode 100644 constantine/bls12_381/benches/groups.rs delete mode 100644 constantine/bls12_381/benches/hash_to_curve.rs delete mode 100644 constantine/bls12_381/src/fp.rs delete mode 100644 constantine/bls12_381/src/fp12.rs delete mode 100644 constantine/bls12_381/src/fp2.rs delete mode 100644 constantine/bls12_381/src/fp6.rs delete mode 100644 constantine/bls12_381/src/g1.rs delete mode 100644 constantine/bls12_381/src/g2.rs delete mode 100644 constantine/bls12_381/src/hash_to_curve/chain.rs delete mode 100644 constantine/bls12_381/src/hash_to_curve/expand_msg.rs delete mode 100644 constantine/bls12_381/src/hash_to_curve/map_g1.rs delete mode 100644 constantine/bls12_381/src/hash_to_curve/map_g2.rs delete mode 100644 constantine/bls12_381/src/hash_to_curve/map_scalar.rs delete mode 100644 constantine/bls12_381/src/hash_to_curve/mod.rs delete mode 100644 constantine/bls12_381/src/lib.rs delete mode 100644 constantine/bls12_381/src/notes/design.rs delete mode 100644 constantine/bls12_381/src/notes/serialization.rs delete mode 100644 constantine/bls12_381/src/pairings.rs delete mode 100644 constantine/bls12_381/src/scalar.rs delete mode 100644 constantine/bls12_381/src/tests/g1_compressed_valid_test_vectors.dat delete mode 100644 constantine/bls12_381/src/tests/g1_uncompressed_valid_test_vectors.dat delete mode 100644 constantine/bls12_381/src/tests/g2_compressed_valid_test_vectors.dat delete mode 100644 constantine/bls12_381/src/tests/g2_uncompressed_valid_test_vectors.dat delete mode 100644 constantine/bls12_381/src/tests/mod.rs delete mode 100644 constantine/bls12_381/src/util.rs delete mode 100644 constantine/src/das.rs create mode 100644 constantine/src/data_availability_sampling.rs delete mode 100644 constantine/src/fft.rs create mode 100644 constantine/src/fft_fr.rs delete mode 100644 constantine/src/kzg_types.rs delete mode 100644 constantine/src/multiscalar_mul.rs delete mode 100644 constantine/src/recover.rs create mode 100644 constantine/src/recovery.rs create mode 100644 constantine/src/types/fft_settings.rs create mode 100644 constantine/src/types/fk20_multi_settings.rs create mode 100644 constantine/src/types/fk20_single_settings.rs create mode 100644 constantine/src/types/fr.rs create mode 100644 constantine/src/types/g1.rs create mode 100644 constantine/src/types/g2.rs create mode 100644 constantine/src/types/kzg_settings.rs create mode 100644 constantine/src/types/mod.rs rename constantine/src/{ => types}/poly.rs (85%) create mode 100644 constantine/tests/c_bindings.rs create mode 100644 constantine/tests/local_tests/local_consts.rs create mode 100644 constantine/tests/local_tests/local_poly.rs create mode 100644 constantine/tests/local_tests/local_recovery.rs create mode 100644 constantine/tests/local_tests/mod.rs create mode 100644 constantine/tests/mod.rs delete mode 100644 constantine/tests/recover.rs create mode 100644 constantine/tests/recovery.rs diff --git a/Cargo.lock b/Cargo.lock index b6a2f1cdf..02021ad69 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -397,7 +397,7 @@ checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" [[package]] name = "constantine-sys" version = "0.1.0" -source = "git+https://github.com/mratsim/constantine.git#8367d7d19cdbba874aab961b70d272e742184c37" +source = "git+https://github.com/mratsim/constantine.git?rev=0afccb412e2b0d2a182d13d1f42f6528307802ae#0afccb412e2b0d2a182d13d1f42f6528307802ae" [[package]] name = "cpufeatures" @@ -1215,19 +1215,16 @@ dependencies = [ name = "rust-kzg-constantine" version = "0.1.0" dependencies = [ - "bls12_381", - "blst", - "byteorder", "constantine-sys", "criterion 0.5.1", - "ff", "hex", "kzg", "kzg-bench", "libc", + "once_cell", "rand", "rayon", - "subtle", + "smallvec", ] [[package]] diff --git a/constantine/.gitignore b/constantine/.gitignore new file mode 100644 index 000000000..ab87df8d4 --- /dev/null +++ b/constantine/.gitignore @@ -0,0 +1 @@ +c-kzg-4844/ \ No newline at end of file diff --git a/constantine/Cargo.toml b/constantine/Cargo.toml index 51287c504..1d77e6d67 100644 --- a/constantine/Cargo.toml +++ b/constantine/Cargo.toml @@ -4,46 +4,46 @@ version = "0.1.0" edition = "2021" [dependencies] -blst = {'git' = 'https://github.com/supranational/blst.git'} kzg = { path = "../kzg", default-features = false } -constantine-sys = { 'git' = 'https://github.com/mratsim/constantine.git' } -bls12_381 = { path = "../zkcrypto/bls12_381" } -ff = { version = "0.13", features = ["derive"] } -hex = "0.4.3" -rand = { version = "0.8.5", optional = true } libc = { version = "0.2.148", default-features = false } -rayon = { version = "1.8.0", optional = true } -subtle = "2.5.0" -byteorder = "1.5.0" +once_cell = { version = "1.18.0", features = ["critical-section"], default-features = false } +constantine-sys = { 'git' = 'https://github.com/mratsim/constantine.git', rev = "0afccb412e2b0d2a182d13d1f42f6528307802ae" } +rand = { version = "0.8.5", optional = true } +rayon = { version = "1.8.0", optional = true } +smallvec = { version = "1.11.1", features = ["const_generics"] } +hex = { version = "0.4.3", default-features = false, features = ["alloc"] } [dev-dependencies] criterion = "0.5.1" kzg-bench = { path = "../kzg-bench" } -rand = { version = "0.8.5" } +rand = "0.8.5" [features] default = [ "std", - "rand" + "rand", ] std = [ + "hex/std", "kzg/std", - "libc/std" -] -parallel = [ - "dep:rayon", "kzg/parallel" + "libc/std", + "once_cell/std", ] rand = [ "dep:rand", "kzg/rand", ] +parallel = [ + "dep:rayon", + "kzg/parallel" +] [[bench]] -name = "fft" +name = "das" harness = false [[bench]] -name = "kzg" +name = "fft" harness = false [[bench]] @@ -51,7 +51,7 @@ name = "poly" harness = false [[bench]] -name = "das" +name = "kzg" harness = false [[bench]] @@ -73,4 +73,3 @@ harness = false [[bench]] name = "lincomb" harness = false - diff --git a/constantine/README.md b/constantine/README.md new file mode 100644 index 000000000..2802da0e4 --- /dev/null +++ b/constantine/README.md @@ -0,0 +1,46 @@ +# KZG10-BLST +## [BLS12-381 lib](https://github.com/supranational/blst) + +Planned functionality: + * Finite field FFT (Fast Fourier Transform); + * G1 group FFT; + * Data availability sampling (data extension, recovery from any 1/2 samples); + * Zero polynomials; + * KZG10 single commit/verify; + * KZG10 multi commit/verify; + * FK20 single proof regular/optimised for DAS; + * FK20 multi proof regular/optimised for DAS. + * Test suite (based on C implementation) + * Benchmarks + +## Other implementations +* [C implementation](https://github.com/benjaminion/c-kzg) +* [Go implementation](https://github.com/protolambda/go-kzg) +* [Rust herumi implementation](https://github.com/UndeadRat22/kzg10-rust) + +``` + .-. .-. + / \ / \ + | _ \ / _ | + ; | \ \ / / | ; + \ \ \ \_.._/ / / / + '. '.;' ';,' .' + './ _ _ \.' + .' a __ a '. + '--./ _, \/ ,_ \.--' + ----| \ /\ / |---- + .--'\ '-' '-' /'--. + _>.__ -- _.- `; + .' _ __/ _/ + / '.,:".-\ /:, + | \.' `""`'.\\ + '-,.__/ _ .-. ;|_ + /` `|| _/ `\/_ \_|| `\ + | ||/ \-./` \ / || | + \ ||__/__|___|__|| / + \_ |__BLS12-381__| / + .' \ = _= _ = _= /`\ + / `-;----=--;--' \ + \ _.-' '. / + `""` Happy Easter `""` +``` diff --git a/constantine/benches/das.rs b/constantine/benches/das.rs index 725f3e429..c88352846 100644 --- a/constantine/benches/das.rs +++ b/constantine/benches/das.rs @@ -1,10 +1,10 @@ use criterion::{criterion_group, criterion_main, Criterion}; use kzg_bench::benches::das::bench_das_extension; -use rust_kzg_zkcrypto::kzg_proofs::FFTSettings; -use rust_kzg_zkcrypto::kzg_types::ZFr; +use rust_kzg_blst::types::fft_settings::CtFFTSettings; +use rust_kzg_blst::types::fr::CtFr; fn bench_das_extension_(c: &mut Criterion) { - bench_das_extension::(c); + bench_das_extension::(c) } criterion_group! { diff --git a/constantine/benches/eip_4844.rs b/constantine/benches/eip_4844.rs index 79d0116f4..a33c00754 100644 --- a/constantine/benches/eip_4844.rs +++ b/constantine/benches/eip_4844.rs @@ -5,13 +5,16 @@ use kzg::eip_4844::{ verify_kzg_proof_rust, }; use kzg_bench::benches::eip_4844::bench_eip_4844; -use rust_kzg_zkcrypto::eip_4844::load_trusted_setup_filename_rust; -use rust_kzg_zkcrypto::kzg_proofs::{FFTSettings, KZGSettings}; -use rust_kzg_zkcrypto::kzg_types::{ZFr, ZG1, ZG2}; -use rust_kzg_zkcrypto::poly::PolyData; +use rust_kzg_blst::{ + eip_4844::load_trusted_setup_filename_rust, + types::{ + fft_settings::CtFFTSettings, fr::CtFr, g1::CtG1, g2::CtG2, kzg_settings::CtKZGSettings, + poly::CtPoly, + }, +}; fn bench_eip_4844_(c: &mut Criterion) { - bench_eip_4844::( + bench_eip_4844::( c, &load_trusted_setup_filename_rust, &blob_to_kzg_commitment_rust, @@ -24,5 +27,5 @@ fn bench_eip_4844_(c: &mut Criterion) { ); } -criterion_group!(benches, bench_eip_4844_,); +criterion_group!(benches, bench_eip_4844_); criterion_main!(benches); diff --git a/constantine/benches/fft.rs b/constantine/benches/fft.rs index de1d4cdf1..0f1231394 100644 --- a/constantine/benches/fft.rs +++ b/constantine/benches/fft.rs @@ -1,14 +1,15 @@ use criterion::{criterion_group, criterion_main, Criterion}; use kzg_bench::benches::fft::{bench_fft_fr, bench_fft_g1}; -use rust_kzg_zkcrypto::kzg_proofs::FFTSettings; -use rust_kzg_zkcrypto::kzg_types::{ZFr, ZG1}; +use rust_kzg_blst::types::fft_settings::CtFFTSettings; +use rust_kzg_blst::types::fr::CtFr; +use rust_kzg_blst::types::g1::CtG1; fn bench_fft_fr_(c: &mut Criterion) { - bench_fft_fr::(c); + bench_fft_fr::(c); } fn bench_fft_g1_(c: &mut Criterion) { - bench_fft_g1::(c); + bench_fft_g1::(c); } criterion_group! { diff --git a/constantine/benches/fk_20.rs b/constantine/benches/fk_20.rs index 5e1292e36..11db1f73e 100644 --- a/constantine/benches/fk_20.rs +++ b/constantine/benches/fk_20.rs @@ -1,19 +1,25 @@ use criterion::{criterion_group, criterion_main, Criterion}; use kzg_bench::benches::fk20::{bench_fk_multi_da, bench_fk_single_da}; -use rust_kzg_zkcrypto::fk20_proofs::{KzgFK20MultiSettings, KzgFK20SingleSettings}; -use rust_kzg_zkcrypto::kzg_proofs::{generate_trusted_setup, FFTSettings, KZGSettings}; -use rust_kzg_zkcrypto::kzg_types::{ZFr, ZG1, ZG2}; -use rust_kzg_zkcrypto::poly::PolyData; + +use rust_kzg_blst::types::fft_settings::CtFFTSettings; +use rust_kzg_blst::types::fk20_multi_settings::CtFK20MultiSettings; +use rust_kzg_blst::types::fk20_single_settings::CtFK20SingleSettings; +use rust_kzg_blst::types::fr::CtFr; +use rust_kzg_blst::types::g1::CtG1; +use rust_kzg_blst::types::g2::CtG2; +use rust_kzg_blst::types::kzg_settings::CtKZGSettings; +use rust_kzg_blst::types::poly::CtPoly; +use rust_kzg_blst::utils::generate_trusted_setup; fn bench_fk_single_da_(c: &mut Criterion) { - bench_fk_single_da::( + bench_fk_single_da::( c, &generate_trusted_setup, ) } fn bench_fk_multi_da_(c: &mut Criterion) { - bench_fk_multi_da::( + bench_fk_multi_da::( c, &generate_trusted_setup, ) diff --git a/constantine/benches/kzg.rs b/constantine/benches/kzg.rs index d8ac61ff8..e9d39ebbc 100644 --- a/constantine/benches/kzg.rs +++ b/constantine/benches/kzg.rs @@ -1,22 +1,25 @@ use criterion::{criterion_group, criterion_main, Criterion}; use kzg_bench::benches::kzg::{bench_commit_to_poly, bench_compute_proof_single}; - -use rust_kzg_zkcrypto::kzg_proofs::{generate_trusted_setup, FFTSettings, KZGSettings}; -use rust_kzg_zkcrypto::kzg_types::{ZFr, ZG1, ZG2}; -use rust_kzg_zkcrypto::poly::PolyData; +use rust_kzg_blst::types::fft_settings::CtFFTSettings; +use rust_kzg_blst::types::fr::CtFr; +use rust_kzg_blst::types::g1::CtG1; +use rust_kzg_blst::types::g2::CtG2; +use rust_kzg_blst::types::kzg_settings::CtKZGSettings; +use rust_kzg_blst::types::poly::CtPoly; +use rust_kzg_blst::utils::generate_trusted_setup; fn bench_commit_to_poly_(c: &mut Criterion) { - bench_commit_to_poly::( + bench_commit_to_poly::( c, &generate_trusted_setup, - ); + ) } fn bench_compute_proof_single_(c: &mut Criterion) { - bench_compute_proof_single::( + bench_compute_proof_single::( c, &generate_trusted_setup, - ); + ) } criterion_group! { diff --git a/constantine/benches/lincomb.rs b/constantine/benches/lincomb.rs index 11118d489..d020b0dae 100644 --- a/constantine/benches/lincomb.rs +++ b/constantine/benches/lincomb.rs @@ -1,10 +1,11 @@ use criterion::{criterion_group, criterion_main, Criterion}; use kzg_bench::benches::lincomb::bench_g1_lincomb; -use rust_kzg_zkcrypto::fft_g1::g1_linear_combination; -use rust_kzg_zkcrypto::kzg_types::{ZFr, ZG1}; +use rust_kzg_blst::kzg_prooCt::g1_linear_combination; +use rust_kzg_blst::types::fr::CtFr; +use rust_kzg_blst::types::g1::CtG1; fn bench_g1_lincomb_(c: &mut Criterion) { - bench_g1_lincomb::(c, &g1_linear_combination); + bench_g1_lincomb::(c, &g1_linear_combination); } criterion_group! { diff --git a/constantine/benches/poly.rs b/constantine/benches/poly.rs index 311f27053..7bd31115f 100644 --- a/constantine/benches/poly.rs +++ b/constantine/benches/poly.rs @@ -1,10 +1,10 @@ use criterion::{criterion_group, criterion_main, Criterion}; use kzg_bench::benches::poly::bench_new_poly_div; -use rust_kzg_zkcrypto::kzg_types::ZFr; -use rust_kzg_zkcrypto::poly::PolyData; +use rust_kzg_blst::types::fr::CtFr; +use rust_kzg_blst::types::poly::CtPoly; fn bench_new_poly_div_(c: &mut Criterion) { - bench_new_poly_div::(c); + bench_new_poly_div::(c); } criterion_group! { diff --git a/constantine/benches/recover.rs b/constantine/benches/recover.rs index 0cd28dc75..86a6359af 100644 --- a/constantine/benches/recover.rs +++ b/constantine/benches/recover.rs @@ -1,12 +1,9 @@ use criterion::{criterion_group, criterion_main, Criterion}; use kzg_bench::benches::recover::bench_recover; +use rust_kzg_blst::types::{fft_settings::CtFFTSettings, fr::CtFr, poly::CtPoly}; -use rust_kzg_zkcrypto::kzg_proofs::FFTSettings; -use rust_kzg_zkcrypto::kzg_types::ZFr; -use rust_kzg_zkcrypto::poly::PolyData; - -fn bench_recover_(c: &mut Criterion) { - bench_recover::(c); +pub fn bench_recover_(c: &mut Criterion) { + bench_recover::(c) } criterion_group! { diff --git a/constantine/benches/zero_poly.rs b/constantine/benches/zero_poly.rs index 6fcc9d964..09ba9c2f1 100644 --- a/constantine/benches/zero_poly.rs +++ b/constantine/benches/zero_poly.rs @@ -1,12 +1,9 @@ use criterion::{criterion_group, criterion_main, Criterion}; use kzg_bench::benches::zero_poly::bench_zero_poly; - -use rust_kzg_zkcrypto::kzg_proofs::FFTSettings; -use rust_kzg_zkcrypto::kzg_types::ZFr; -use rust_kzg_zkcrypto::poly::PolyData; +use rust_kzg_blst::types::{fft_settings::CtFFTSettings, fr::CtFr, poly::CtPoly}; fn bench_zero_poly_(c: &mut Criterion) { - bench_zero_poly::(c); + bench_zero_poly::(c); } criterion_group! { diff --git a/constantine/bls12_381/Cargo.toml b/constantine/bls12_381/Cargo.toml deleted file mode 100644 index 7d46f8329..000000000 --- a/constantine/bls12_381/Cargo.toml +++ /dev/null @@ -1,73 +0,0 @@ -[package] -authors = [ - "Sean Bowe ", - "Jack Grigg ", -] -description = "Implementation of the BLS12-381 pairing-friendly elliptic curve construction" -documentation = "https://docs.rs/bls12_381/" -homepage = "https://github.com/zkcrypto/bls12_381" -license = "MIT/Apache-2.0" -name = "bls12_381" -repository = "https://github.com/zkcrypto/bls12_381" -version = "0.8.0" -edition = "2021" - -[package.metadata.docs.rs] -rustdoc-args = [ "--html-in-header", "katex-header.html" ] - -[dev-dependencies] -csv = ">= 1.0, < 1.2" # csv 1.2 has MSRV 1.60 -criterion = "0.3" -hex = "0.4" -rand_xorshift = "0.3" -sha2 = "0.9" -sha3 = "0.9" - -[[bench]] -name = "groups" -harness = false -required-features = ["groups"] - -[[bench]] -name = "hash_to_curve" -harness = false -required-features = ["experimental"] - -[dependencies.digest] -version = "0.9" -optional = true - -[dependencies.ff] -version = "0.13" -default-features = false - -[dependencies.group] -version = "0.13" -default-features = false -optional = true - -[dependencies.pairing] -version = "0.23" -optional = true - -[dependencies.rand_core] -version = "0.6" -default-features = false - -[dependencies.subtle] -version = "2.2.1" -default-features = false - -[dependencies.zeroize] -version = "1.4" -default-features = false -optional = true - -[features] -default = ["groups", "pairings", "alloc", "bits"] -bits = ["ff/bits"] -groups = ["group"] -pairings = ["groups", "pairing"] -alloc = ["group/alloc"] -experimental = ["digest"] -nightly = ["subtle/nightly"] diff --git a/constantine/bls12_381/benches/groups.rs b/constantine/bls12_381/benches/groups.rs deleted file mode 100644 index 87c80d029..000000000 --- a/constantine/bls12_381/benches/groups.rs +++ /dev/null @@ -1,170 +0,0 @@ -#[macro_use] -extern crate criterion; - -extern crate bls12_381; -use bls12_381::*; - -use criterion::{black_box, Criterion}; - -fn criterion_benchmark(c: &mut Criterion) { - // Pairings - { - let g = G1Affine::generator(); - let h = G2Affine::generator(); - c.bench_function("full pairing", move |b| { - b.iter(|| pairing(black_box(&g), black_box(&h))) - }); - c.bench_function("G2 preparation for pairing", move |b| { - b.iter(|| G2Prepared::from(h)) - }); - let prep = G2Prepared::from(h); - c.bench_function("miller loop for pairing", move |b| { - b.iter(|| multi_miller_loop(&[(&g, &prep)])) - }); - let prep = G2Prepared::from(h); - let r = multi_miller_loop(&[(&g, &prep)]); - c.bench_function("final exponentiation for pairing", move |b| { - b.iter(|| r.final_exponentiation()) - }); - } - // G1Affine - { - let name = "G1Affine"; - let a = G1Affine::generator(); - let s = Scalar::from_raw([1, 2, 3, 4]); - let compressed = [0u8; 48]; - let uncompressed = [0u8; 96]; - c.bench_function(&format!("{} check on curve", name), move |b| { - b.iter(|| black_box(a).is_on_curve()) - }); - c.bench_function(&format!("{} check equality", name), move |b| { - b.iter(|| black_box(a) == black_box(a)) - }); - c.bench_function(&format!("{} scalar multiplication", name), move |b| { - b.iter(|| black_box(a) * black_box(s)) - }); - c.bench_function(&format!("{} subgroup check", name), move |b| { - b.iter(|| black_box(a).is_torsion_free()) - }); - c.bench_function( - &format!("{} deserialize compressed point", name), - move |b| b.iter(|| G1Affine::from_compressed(black_box(&compressed))), - ); - c.bench_function( - &format!("{} deserialize uncompressed point", name), - move |b| b.iter(|| G1Affine::from_uncompressed(black_box(&uncompressed))), - ); - } - - // G1Projective - { - let name = "G1Projective"; - let a = G1Projective::generator(); - let a_affine = G1Affine::generator(); - let s = Scalar::from_raw([1, 2, 3, 4]); - - const N: usize = 10000; - let v = vec![G1Projective::generator(); N]; - let mut q = vec![G1Affine::identity(); N]; - - c.bench_function(&format!("{} check on curve", name), move |b| { - b.iter(|| black_box(a).is_on_curve()) - }); - c.bench_function(&format!("{} check equality", name), move |b| { - b.iter(|| black_box(a) == black_box(a)) - }); - c.bench_function(&format!("{} to affine", name), move |b| { - b.iter(|| G1Affine::from(black_box(a))) - }); - 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)) - }); - c.bench_function(&format!("{} mixed addition", name), move |b| { - b.iter(|| black_box(a).add_mixed(&a_affine)) - }); - c.bench_function(&format!("{} scalar multiplication", name), move |b| { - b.iter(|| black_box(a) * black_box(s)) - }); - c.bench_function(&format!("{} batch to affine n={}", name, N), move |b| { - b.iter(|| { - G1Projective::batch_normalize(black_box(&v), black_box(&mut q)); - black_box(&q)[0] - }) - }); - } - - // G2Affine - { - let name = "G2Affine"; - let a = G2Affine::generator(); - let s = Scalar::from_raw([1, 2, 3, 4]); - let compressed = [0u8; 96]; - let uncompressed = [0u8; 192]; - c.bench_function(&format!("{} check on curve", name), move |b| { - b.iter(|| black_box(a).is_on_curve()) - }); - c.bench_function(&format!("{} check equality", name), move |b| { - b.iter(|| black_box(a) == black_box(a)) - }); - c.bench_function(&format!("{} scalar multiplication", name), move |b| { - b.iter(|| black_box(a) * black_box(s)) - }); - c.bench_function(&format!("{} subgroup check", name), move |b| { - b.iter(|| black_box(a).is_torsion_free()) - }); - c.bench_function( - &format!("{} deserialize compressed point", name), - move |b| b.iter(|| G2Affine::from_compressed(black_box(&compressed))), - ); - c.bench_function( - &format!("{} deserialize uncompressed point", name), - move |b| b.iter(|| G2Affine::from_uncompressed(black_box(&uncompressed))), - ); - } - - // G2Projective - { - let name = "G2Projective"; - let a = G2Projective::generator(); - let a_affine = G2Affine::generator(); - let s = Scalar::from_raw([1, 2, 3, 4]); - - const N: usize = 10000; - let v = vec![G2Projective::generator(); N]; - let mut q = vec![G2Affine::identity(); N]; - - c.bench_function(&format!("{} check on curve", name), move |b| { - b.iter(|| black_box(a).is_on_curve()) - }); - c.bench_function(&format!("{} check equality", name), move |b| { - b.iter(|| black_box(a) == black_box(a)) - }); - c.bench_function(&format!("{} to affine", name), move |b| { - b.iter(|| G2Affine::from(black_box(a))) - }); - 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)) - }); - c.bench_function(&format!("{} mixed addition", name), move |b| { - b.iter(|| black_box(a).add_mixed(&a_affine)) - }); - c.bench_function(&format!("{} scalar multiplication", name), move |b| { - b.iter(|| black_box(a) * black_box(s)) - }); - c.bench_function(&format!("{} batch to affine n={}", name, N), move |b| { - b.iter(|| { - G2Projective::batch_normalize(black_box(&v), black_box(&mut q)); - black_box(&q)[0] - }) - }); - } -} - -criterion_group!(benches, criterion_benchmark); -criterion_main!(benches); diff --git a/constantine/bls12_381/benches/hash_to_curve.rs b/constantine/bls12_381/benches/hash_to_curve.rs deleted file mode 100644 index 302b7c6b3..000000000 --- a/constantine/bls12_381/benches/hash_to_curve.rs +++ /dev/null @@ -1,68 +0,0 @@ -#[macro_use] -extern crate criterion; - -extern crate bls12_381; -use bls12_381::hash_to_curve::*; -use bls12_381::*; - -use criterion::{black_box, Criterion}; - -fn criterion_benchmark(c: &mut Criterion) { - // G1Projective - { - let name = "G1Projective"; - - let message: &[u8] = b"test message"; - let dst: &[u8] = b"test DST"; - - c.bench_function( - &format!("{} encode_to_curve SSWU SHA-256", name), - move |b| { - b.iter(|| { - >>::encode_to_curve( - black_box(message), - black_box(dst), - ) - }) - }, - ); - c.bench_function(&format!("{} hash_to_curve SSWU SHA-256", name), move |b| { - b.iter(|| { - >>::hash_to_curve( - black_box(message), - black_box(dst), - ) - }) - }); - } - // G2Projective - { - let name = "G2Projective"; - - let message: &[u8] = b"test message"; - let dst: &[u8] = b"test DST"; - - c.bench_function( - &format!("{} encode_to_curve SSWU SHA-256", name), - move |b| { - b.iter(|| { - >>::encode_to_curve( - black_box(message), - black_box(dst), - ) - }) - }, - ); - c.bench_function(&format!("{} hash_to_curve SSWU SHA-256", name), move |b| { - b.iter(|| { - >>::hash_to_curve( - black_box(message), - black_box(dst), - ) - }) - }); - } -} - -criterion_group!(benches, criterion_benchmark); -criterion_main!(benches); diff --git a/constantine/bls12_381/src/fp.rs b/constantine/bls12_381/src/fp.rs deleted file mode 100644 index f61150cc6..000000000 --- a/constantine/bls12_381/src/fp.rs +++ /dev/null @@ -1,990 +0,0 @@ -//! This module provides an implementation of the BLS12-381 base field `GF(p)` -//! where `p = 0x1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab` -#![allow(clippy::all)] - -use core::fmt; -use core::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}; -use rand_core::RngCore; -use subtle::{Choice, ConditionallySelectable, ConstantTimeEq, CtOption}; - -use crate::util::{adc, mac, sbb}; - -// The internal representation of this type is six 64-bit unsigned -// integers in little-endian order. `Fp` values are always in -// Montgomery form; i.e., Scalar(a) = aR mod p, with R = 2^384. -#[derive(Copy, Clone)] -pub struct Fp(pub [u64; 6]); - -impl fmt::Debug for Fp { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let tmp = self.to_bytes(); - write!(f, "0x")?; - for &b in tmp.iter() { - write!(f, "{:02x}", b)?; - } - Ok(()) - } -} - -impl Default for Fp { - fn default() -> Self { - Fp::zero() - } -} - -#[cfg(feature = "zeroize")] -impl zeroize::DefaultIsZeroes for Fp {} - -impl ConstantTimeEq for Fp { - fn ct_eq(&self, other: &Self) -> Choice { - self.0[0].ct_eq(&other.0[0]) - & self.0[1].ct_eq(&other.0[1]) - & self.0[2].ct_eq(&other.0[2]) - & self.0[3].ct_eq(&other.0[3]) - & self.0[4].ct_eq(&other.0[4]) - & self.0[5].ct_eq(&other.0[5]) - } -} - -impl Eq for Fp {} -impl PartialEq for Fp { - #[inline] - fn eq(&self, other: &Self) -> bool { - bool::from(self.ct_eq(other)) - } -} - -impl ConditionallySelectable for Fp { - fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self { - Fp([ - u64::conditional_select(&a.0[0], &b.0[0], choice), - u64::conditional_select(&a.0[1], &b.0[1], choice), - u64::conditional_select(&a.0[2], &b.0[2], choice), - u64::conditional_select(&a.0[3], &b.0[3], choice), - u64::conditional_select(&a.0[4], &b.0[4], choice), - u64::conditional_select(&a.0[5], &b.0[5], choice), - ]) - } -} - -/// p = 4002409555221667393417789825735904156556882819939007885332058136124031650490837864442687629129015664037894272559787 -const MODULUS: [u64; 6] = [ - 0xb9fe_ffff_ffff_aaab, - 0x1eab_fffe_b153_ffff, - 0x6730_d2a0_f6b0_f624, - 0x6477_4b84_f385_12bf, - 0x4b1b_a7b6_434b_acd7, - 0x1a01_11ea_397f_e69a, -]; - -/// INV = -(p^{-1} mod 2^64) mod 2^64 -const INV: u64 = 0x89f3_fffc_fffc_fffd; - -/// R = 2^384 mod p -const R: Fp = Fp([ - 0x7609_0000_0002_fffd, - 0xebf4_000b_c40c_0002, - 0x5f48_9857_53c7_58ba, - 0x77ce_5853_7052_5745, - 0x5c07_1a97_a256_ec6d, - 0x15f6_5ec3_fa80_e493, -]); - -/// R2 = 2^(384*2) mod p -const R2: Fp = Fp([ - 0xf4df_1f34_1c34_1746, - 0x0a76_e6a6_09d1_04f1, - 0x8de5_476c_4c95_b6d5, - 0x67eb_88a9_939d_83c0, - 0x9a79_3e85_b519_952d, - 0x1198_8fe5_92ca_e3aa, -]); - -/// R3 = 2^(384*3) mod p -const R3: Fp = Fp([ - 0xed48_ac6b_d94c_a1e0, - 0x315f_831e_03a7_adf8, - 0x9a53_352a_615e_29dd, - 0x34c0_4e5e_921e_1761, - 0x2512_d435_6572_4728, - 0x0aa6_3460_9175_5d4d, -]); - -impl<'a> Neg for &'a Fp { - type Output = Fp; - - #[inline] - fn neg(self) -> Fp { - self.neg() - } -} - -impl Neg for Fp { - type Output = Fp; - - #[inline] - fn neg(self) -> Fp { - -&self - } -} - -impl<'a, 'b> Sub<&'b Fp> for &'a Fp { - type Output = Fp; - - #[inline] - fn sub(self, rhs: &'b Fp) -> Fp { - self.sub(rhs) - } -} - -impl<'a, 'b> Add<&'b Fp> for &'a Fp { - type Output = Fp; - - #[inline] - fn add(self, rhs: &'b Fp) -> Fp { - self.add(rhs) - } -} - -impl<'a, 'b> Mul<&'b Fp> for &'a Fp { - type Output = Fp; - - #[inline] - fn mul(self, rhs: &'b Fp) -> Fp { - self.mul(rhs) - } -} - -impl_binops_additive!(Fp, Fp); -impl_binops_multiplicative!(Fp, Fp); - -impl Fp { - /// Returns zero, the additive identity. - #[inline] - pub const fn zero() -> Fp { - Fp([0, 0, 0, 0, 0, 0]) - } - - /// Returns one, the multiplicative identity. - #[inline] - pub const fn one() -> Fp { - R - } - - pub fn is_zero(&self) -> Choice { - self.ct_eq(&Fp::zero()) - } - - /// Attempts to convert a big-endian byte representation of - /// a scalar into an `Fp`, failing if the input is not canonical. - pub fn from_bytes(bytes: &[u8; 48]) -> CtOption { - let mut tmp = Fp([0, 0, 0, 0, 0, 0]); - - tmp.0[5] = u64::from_be_bytes(<[u8; 8]>::try_from(&bytes[0..8]).unwrap()); - tmp.0[4] = u64::from_be_bytes(<[u8; 8]>::try_from(&bytes[8..16]).unwrap()); - tmp.0[3] = u64::from_be_bytes(<[u8; 8]>::try_from(&bytes[16..24]).unwrap()); - tmp.0[2] = u64::from_be_bytes(<[u8; 8]>::try_from(&bytes[24..32]).unwrap()); - tmp.0[1] = u64::from_be_bytes(<[u8; 8]>::try_from(&bytes[32..40]).unwrap()); - tmp.0[0] = u64::from_be_bytes(<[u8; 8]>::try_from(&bytes[40..48]).unwrap()); - - // Try to subtract the modulus - let (_, borrow) = sbb(tmp.0[0], MODULUS[0], 0); - let (_, borrow) = sbb(tmp.0[1], MODULUS[1], borrow); - let (_, borrow) = sbb(tmp.0[2], MODULUS[2], borrow); - let (_, borrow) = sbb(tmp.0[3], MODULUS[3], borrow); - let (_, borrow) = sbb(tmp.0[4], MODULUS[4], borrow); - let (_, borrow) = sbb(tmp.0[5], MODULUS[5], borrow); - - // If the element is smaller than MODULUS then the - // subtraction will underflow, producing a borrow value - // of 0xffff...ffff. Otherwise, it'll be zero. - let is_some = (borrow as u8) & 1; - - // Convert to Montgomery form by computing - // (a.R^0 * R^2) / R = a.R - tmp *= &R2; - - CtOption::new(tmp, Choice::from(is_some)) - } - - /// Converts an element of `Fp` into a byte representation in - /// big-endian byte order. - pub fn to_bytes(self) -> [u8; 48] { - // Turn into canonical form by computing - // (a.R) / R = a - let tmp = Fp::montgomery_reduce( - self.0[0], self.0[1], self.0[2], self.0[3], self.0[4], self.0[5], 0, 0, 0, 0, 0, 0, - ); - - let mut res = [0; 48]; - res[0..8].copy_from_slice(&tmp.0[5].to_be_bytes()); - res[8..16].copy_from_slice(&tmp.0[4].to_be_bytes()); - res[16..24].copy_from_slice(&tmp.0[3].to_be_bytes()); - res[24..32].copy_from_slice(&tmp.0[2].to_be_bytes()); - res[32..40].copy_from_slice(&tmp.0[1].to_be_bytes()); - res[40..48].copy_from_slice(&tmp.0[0].to_be_bytes()); - - res - } - - pub(crate) fn random(mut rng: impl RngCore) -> Fp { - let mut bytes = [0u8; 96]; - rng.fill_bytes(&mut bytes); - - // Parse the random bytes as a big-endian number, to match Fp encoding order. - Fp::from_u768([ - u64::from_be_bytes(<[u8; 8]>::try_from(&bytes[0..8]).unwrap()), - u64::from_be_bytes(<[u8; 8]>::try_from(&bytes[8..16]).unwrap()), - u64::from_be_bytes(<[u8; 8]>::try_from(&bytes[16..24]).unwrap()), - u64::from_be_bytes(<[u8; 8]>::try_from(&bytes[24..32]).unwrap()), - u64::from_be_bytes(<[u8; 8]>::try_from(&bytes[32..40]).unwrap()), - u64::from_be_bytes(<[u8; 8]>::try_from(&bytes[40..48]).unwrap()), - u64::from_be_bytes(<[u8; 8]>::try_from(&bytes[48..56]).unwrap()), - u64::from_be_bytes(<[u8; 8]>::try_from(&bytes[56..64]).unwrap()), - u64::from_be_bytes(<[u8; 8]>::try_from(&bytes[64..72]).unwrap()), - u64::from_be_bytes(<[u8; 8]>::try_from(&bytes[72..80]).unwrap()), - u64::from_be_bytes(<[u8; 8]>::try_from(&bytes[80..88]).unwrap()), - u64::from_be_bytes(<[u8; 8]>::try_from(&bytes[88..96]).unwrap()), - ]) - } - - /// Reduces a big-endian 64-bit limb representation of a 768-bit number. - fn from_u768(limbs: [u64; 12]) -> Fp { - // We reduce an arbitrary 768-bit number by decomposing it into two 384-bit digits - // with the higher bits multiplied by 2^384. Thus, we perform two reductions - // - // 1. the lower bits are multiplied by R^2, as normal - // 2. the upper bits are multiplied by R^2 * 2^384 = R^3 - // - // and computing their sum in the field. It remains to see that arbitrary 384-bit - // numbers can be placed into Montgomery form safely using the reduction. The - // reduction works so long as the product is less than R=2^384 multiplied by - // the modulus. This holds because for any `c` smaller than the modulus, we have - // that (2^384 - 1)*c is an acceptable product for the reduction. Therefore, the - // reduction always works so long as `c` is in the field; in this case it is either the - // constant `R2` or `R3`. - let d1 = Fp([limbs[11], limbs[10], limbs[9], limbs[8], limbs[7], limbs[6]]); - let d0 = Fp([limbs[5], limbs[4], limbs[3], limbs[2], limbs[1], limbs[0]]); - // Convert to Montgomery form - d0 * R2 + d1 * R3 - } - - /// Returns whether or not this element is strictly lexicographically - /// larger than its negation. - pub fn lexicographically_largest(&self) -> Choice { - // This can be determined by checking to see if the element is - // larger than (p - 1) // 2. If we subtract by ((p - 1) // 2) + 1 - // and there is no underflow, then the element must be larger than - // (p - 1) // 2. - - // First, because self is in Montgomery form we need to reduce it - let tmp = Fp::montgomery_reduce( - self.0[0], self.0[1], self.0[2], self.0[3], self.0[4], self.0[5], 0, 0, 0, 0, 0, 0, - ); - - let (_, borrow) = sbb(tmp.0[0], 0xdcff_7fff_ffff_d556, 0); - let (_, borrow) = sbb(tmp.0[1], 0x0f55_ffff_58a9_ffff, borrow); - let (_, borrow) = sbb(tmp.0[2], 0xb398_6950_7b58_7b12, borrow); - let (_, borrow) = sbb(tmp.0[3], 0xb23b_a5c2_79c2_895f, borrow); - let (_, borrow) = sbb(tmp.0[4], 0x258d_d3db_21a5_d66b, borrow); - let (_, borrow) = sbb(tmp.0[5], 0x0d00_88f5_1cbf_f34d, borrow); - - // If the element was smaller, the subtraction will underflow - // producing a borrow value of 0xffff...ffff, otherwise it will - // be zero. We create a Choice representing true if there was - // overflow (and so this element is not lexicographically larger - // than its negation) and then negate it. - - !Choice::from((borrow as u8) & 1) - } - - /// Constructs an element of `Fp` without checking that it is - /// canonical. - pub const fn from_raw_unchecked(v: [u64; 6]) -> Fp { - Fp(v) - } - - /// Although this is labeled "vartime", it is only - /// variable time with respect to the exponent. It - /// is also not exposed in the public API. - pub fn pow_vartime(&self, by: &[u64; 6]) -> Self { - let mut res = Self::one(); - for e in by.iter().rev() { - for i in (0..64).rev() { - res = res.square(); - - if ((*e >> i) & 1) == 1 { - res *= self; - } - } - } - res - } - - #[inline] - pub fn sqrt(&self) -> CtOption { - // We use Shank's method, as p = 3 (mod 4). This means - // we only need to exponentiate by (p+1)/4. This only - // works for elements that are actually quadratic residue, - // so we check that we got the correct result at the end. - - let sqrt = self.pow_vartime(&[ - 0xee7f_bfff_ffff_eaab, - 0x07aa_ffff_ac54_ffff, - 0xd9cc_34a8_3dac_3d89, - 0xd91d_d2e1_3ce1_44af, - 0x92c6_e9ed_90d2_eb35, - 0x0680_447a_8e5f_f9a6, - ]); - - CtOption::new(sqrt, sqrt.square().ct_eq(self)) - } - - #[inline] - /// Computes the multiplicative inverse of this field - /// element, returning None in the case that this element - /// is zero. - pub fn invert(&self) -> CtOption { - // Exponentiate by p - 2 - let t = self.pow_vartime(&[ - 0xb9fe_ffff_ffff_aaa9, - 0x1eab_fffe_b153_ffff, - 0x6730_d2a0_f6b0_f624, - 0x6477_4b84_f385_12bf, - 0x4b1b_a7b6_434b_acd7, - 0x1a01_11ea_397f_e69a, - ]); - - CtOption::new(t, !self.is_zero()) - } - - #[inline] - const fn subtract_p(&self) -> Fp { - let (r0, borrow) = sbb(self.0[0], MODULUS[0], 0); - let (r1, borrow) = sbb(self.0[1], MODULUS[1], borrow); - let (r2, borrow) = sbb(self.0[2], MODULUS[2], borrow); - let (r3, borrow) = sbb(self.0[3], MODULUS[3], borrow); - let (r4, borrow) = sbb(self.0[4], MODULUS[4], borrow); - let (r5, borrow) = sbb(self.0[5], MODULUS[5], borrow); - - // If underflow occurred on the final limb, borrow = 0xfff...fff, otherwise - // borrow = 0x000...000. Thus, we use it as a mask! - let r0 = (self.0[0] & borrow) | (r0 & !borrow); - let r1 = (self.0[1] & borrow) | (r1 & !borrow); - let r2 = (self.0[2] & borrow) | (r2 & !borrow); - let r3 = (self.0[3] & borrow) | (r3 & !borrow); - let r4 = (self.0[4] & borrow) | (r4 & !borrow); - let r5 = (self.0[5] & borrow) | (r5 & !borrow); - - Fp([r0, r1, r2, r3, r4, r5]) - } - - #[inline] - pub const fn add(&self, rhs: &Fp) -> Fp { - let (d0, carry) = adc(self.0[0], rhs.0[0], 0); - let (d1, carry) = adc(self.0[1], rhs.0[1], carry); - let (d2, carry) = adc(self.0[2], rhs.0[2], carry); - let (d3, carry) = adc(self.0[3], rhs.0[3], carry); - let (d4, carry) = adc(self.0[4], rhs.0[4], carry); - let (d5, _) = adc(self.0[5], rhs.0[5], carry); - - // Attempt to subtract the modulus, to ensure the value - // is smaller than the modulus. - (&Fp([d0, d1, d2, d3, d4, d5])).subtract_p() - } - - #[inline] - pub const fn neg(&self) -> Fp { - let (d0, borrow) = sbb(MODULUS[0], self.0[0], 0); - let (d1, borrow) = sbb(MODULUS[1], self.0[1], borrow); - let (d2, borrow) = sbb(MODULUS[2], self.0[2], borrow); - let (d3, borrow) = sbb(MODULUS[3], self.0[3], borrow); - let (d4, borrow) = sbb(MODULUS[4], self.0[4], borrow); - let (d5, _) = sbb(MODULUS[5], self.0[5], borrow); - - // Let's use a mask if `self` was zero, which would mean - // the result of the subtraction is p. - let mask = (((self.0[0] | self.0[1] | self.0[2] | self.0[3] | self.0[4] | self.0[5]) == 0) - as u64) - .wrapping_sub(1); - - Fp([ - d0 & mask, - d1 & mask, - d2 & mask, - d3 & mask, - d4 & mask, - d5 & mask, - ]) - } - - #[inline] - pub const fn sub(&self, rhs: &Fp) -> Fp { - (&rhs.neg()).add(self) - } - - /// Returns `c = a.zip(b).fold(0, |acc, (a_i, b_i)| acc + a_i * b_i)`. - /// - /// Implements Algorithm 2 from Patrick Longa's - /// [ePrint 2022-367](https://eprint.iacr.org/2022/367) ยง3. - #[inline] - pub(crate) fn sum_of_products(a: [Fp; T], b: [Fp; T]) -> Fp { - // For a single `a x b` multiplication, operand scanning (schoolbook) takes each - // limb of `a` in turn, and multiplies it by all of the limbs of `b` to compute - // the result as a double-width intermediate representation, which is then fully - // reduced at the end. Here however we have pairs of multiplications (a_i, b_i), - // the results of which are summed. - // - // The intuition for this algorithm is two-fold: - // - We can interleave the operand scanning for each pair, by processing the jth - // limb of each `a_i` together. As these have the same offset within the overall - // operand scanning flow, their results can be summed directly. - // - We can interleave the multiplication and reduction steps, resulting in a - // single bitshift by the limb size after each iteration. This means we only - // need to store a single extra limb overall, instead of keeping around all the - // intermediate results and eventually having twice as many limbs. - - // Algorithm 2, line 2 - let (u0, u1, u2, u3, u4, u5) = - (0..6).fold((0, 0, 0, 0, 0, 0), |(u0, u1, u2, u3, u4, u5), j| { - // Algorithm 2, line 3 - // For each pair in the overall sum of products: - let (t0, t1, t2, t3, t4, t5, t6) = (0..T).fold( - (u0, u1, u2, u3, u4, u5, 0), - |(t0, t1, t2, t3, t4, t5, t6), i| { - // Compute digit_j x row and accumulate into `u`. - let (t0, carry) = mac(t0, a[i].0[j], b[i].0[0], 0); - let (t1, carry) = mac(t1, a[i].0[j], b[i].0[1], carry); - let (t2, carry) = mac(t2, a[i].0[j], b[i].0[2], carry); - let (t3, carry) = mac(t3, a[i].0[j], b[i].0[3], carry); - let (t4, carry) = mac(t4, a[i].0[j], b[i].0[4], carry); - let (t5, carry) = mac(t5, a[i].0[j], b[i].0[5], carry); - let (t6, _) = adc(t6, 0, carry); - - (t0, t1, t2, t3, t4, t5, t6) - }, - ); - - // Algorithm 2, lines 4-5 - // This is a single step of the usual Montgomery reduction process. - let k = t0.wrapping_mul(INV); - let (_, carry) = mac(t0, k, MODULUS[0], 0); - let (r1, carry) = mac(t1, k, MODULUS[1], carry); - let (r2, carry) = mac(t2, k, MODULUS[2], carry); - let (r3, carry) = mac(t3, k, MODULUS[3], carry); - let (r4, carry) = mac(t4, k, MODULUS[4], carry); - let (r5, carry) = mac(t5, k, MODULUS[5], carry); - let (r6, _) = adc(t6, 0, carry); - - (r1, r2, r3, r4, r5, r6) - }); - - // Because we represent F_p elements in non-redundant form, we need a final - // conditional subtraction to ensure the output is in range. - (&Fp([u0, u1, u2, u3, u4, u5])).subtract_p() - } - - #[inline(always)] - pub(crate) const fn montgomery_reduce( - t0: u64, - t1: u64, - t2: u64, - t3: u64, - t4: u64, - t5: u64, - t6: u64, - t7: u64, - t8: u64, - t9: u64, - t10: u64, - t11: u64, - ) -> Self { - // The Montgomery reduction here is based on Algorithm 14.32 in - // Handbook of Applied Cryptography - // . - - let k = t0.wrapping_mul(INV); - let (_, carry) = mac(t0, k, MODULUS[0], 0); - let (r1, carry) = mac(t1, k, MODULUS[1], carry); - let (r2, carry) = mac(t2, k, MODULUS[2], carry); - let (r3, carry) = mac(t3, k, MODULUS[3], carry); - let (r4, carry) = mac(t4, k, MODULUS[4], carry); - let (r5, carry) = mac(t5, k, MODULUS[5], carry); - let (r6, r7) = adc(t6, 0, carry); - - let k = r1.wrapping_mul(INV); - let (_, carry) = mac(r1, k, MODULUS[0], 0); - let (r2, carry) = mac(r2, k, MODULUS[1], carry); - let (r3, carry) = mac(r3, k, MODULUS[2], carry); - let (r4, carry) = mac(r4, k, MODULUS[3], carry); - let (r5, carry) = mac(r5, k, MODULUS[4], carry); - let (r6, carry) = mac(r6, k, MODULUS[5], carry); - let (r7, r8) = adc(t7, r7, carry); - - let k = r2.wrapping_mul(INV); - let (_, carry) = mac(r2, k, MODULUS[0], 0); - let (r3, carry) = mac(r3, k, MODULUS[1], carry); - let (r4, carry) = mac(r4, k, MODULUS[2], carry); - let (r5, carry) = mac(r5, k, MODULUS[3], carry); - let (r6, carry) = mac(r6, k, MODULUS[4], carry); - let (r7, carry) = mac(r7, k, MODULUS[5], carry); - let (r8, r9) = adc(t8, r8, carry); - - let k = r3.wrapping_mul(INV); - let (_, carry) = mac(r3, k, MODULUS[0], 0); - let (r4, carry) = mac(r4, k, MODULUS[1], carry); - let (r5, carry) = mac(r5, k, MODULUS[2], carry); - let (r6, carry) = mac(r6, k, MODULUS[3], carry); - let (r7, carry) = mac(r7, k, MODULUS[4], carry); - let (r8, carry) = mac(r8, k, MODULUS[5], carry); - let (r9, r10) = adc(t9, r9, carry); - - let k = r4.wrapping_mul(INV); - let (_, carry) = mac(r4, k, MODULUS[0], 0); - let (r5, carry) = mac(r5, k, MODULUS[1], carry); - let (r6, carry) = mac(r6, k, MODULUS[2], carry); - let (r7, carry) = mac(r7, k, MODULUS[3], carry); - let (r8, carry) = mac(r8, k, MODULUS[4], carry); - let (r9, carry) = mac(r9, k, MODULUS[5], carry); - let (r10, r11) = adc(t10, r10, carry); - - let k = r5.wrapping_mul(INV); - let (_, carry) = mac(r5, k, MODULUS[0], 0); - let (r6, carry) = mac(r6, k, MODULUS[1], carry); - let (r7, carry) = mac(r7, k, MODULUS[2], carry); - let (r8, carry) = mac(r8, k, MODULUS[3], carry); - let (r9, carry) = mac(r9, k, MODULUS[4], carry); - let (r10, carry) = mac(r10, k, MODULUS[5], carry); - let (r11, _) = adc(t11, r11, carry); - - // Attempt to subtract the modulus, to ensure the value - // is smaller than the modulus. - (&Fp([r6, r7, r8, r9, r10, r11])).subtract_p() - } - - #[inline] - pub const fn mul(&self, rhs: &Fp) -> Fp { - let (t0, carry) = mac(0, self.0[0], rhs.0[0], 0); - let (t1, carry) = mac(0, self.0[0], rhs.0[1], carry); - let (t2, carry) = mac(0, self.0[0], rhs.0[2], carry); - let (t3, carry) = mac(0, self.0[0], rhs.0[3], carry); - let (t4, carry) = mac(0, self.0[0], rhs.0[4], carry); - let (t5, t6) = mac(0, self.0[0], rhs.0[5], carry); - - let (t1, carry) = mac(t1, self.0[1], rhs.0[0], 0); - let (t2, carry) = mac(t2, self.0[1], rhs.0[1], carry); - let (t3, carry) = mac(t3, self.0[1], rhs.0[2], carry); - let (t4, carry) = mac(t4, self.0[1], rhs.0[3], carry); - let (t5, carry) = mac(t5, self.0[1], rhs.0[4], carry); - let (t6, t7) = mac(t6, self.0[1], rhs.0[5], carry); - - let (t2, carry) = mac(t2, self.0[2], rhs.0[0], 0); - let (t3, carry) = mac(t3, self.0[2], rhs.0[1], carry); - let (t4, carry) = mac(t4, self.0[2], rhs.0[2], carry); - let (t5, carry) = mac(t5, self.0[2], rhs.0[3], carry); - let (t6, carry) = mac(t6, self.0[2], rhs.0[4], carry); - let (t7, t8) = mac(t7, self.0[2], rhs.0[5], carry); - - let (t3, carry) = mac(t3, self.0[3], rhs.0[0], 0); - let (t4, carry) = mac(t4, self.0[3], rhs.0[1], carry); - let (t5, carry) = mac(t5, self.0[3], rhs.0[2], carry); - let (t6, carry) = mac(t6, self.0[3], rhs.0[3], carry); - let (t7, carry) = mac(t7, self.0[3], rhs.0[4], carry); - let (t8, t9) = mac(t8, self.0[3], rhs.0[5], carry); - - let (t4, carry) = mac(t4, self.0[4], rhs.0[0], 0); - let (t5, carry) = mac(t5, self.0[4], rhs.0[1], carry); - let (t6, carry) = mac(t6, self.0[4], rhs.0[2], carry); - let (t7, carry) = mac(t7, self.0[4], rhs.0[3], carry); - let (t8, carry) = mac(t8, self.0[4], rhs.0[4], carry); - let (t9, t10) = mac(t9, self.0[4], rhs.0[5], carry); - - let (t5, carry) = mac(t5, self.0[5], rhs.0[0], 0); - let (t6, carry) = mac(t6, self.0[5], rhs.0[1], carry); - let (t7, carry) = mac(t7, self.0[5], rhs.0[2], carry); - let (t8, carry) = mac(t8, self.0[5], rhs.0[3], carry); - let (t9, carry) = mac(t9, self.0[5], rhs.0[4], carry); - let (t10, t11) = mac(t10, self.0[5], rhs.0[5], carry); - - Self::montgomery_reduce(t0, t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11) - } - - /// Squares this element. - #[inline] - pub const fn square(&self) -> Self { - let (t1, carry) = mac(0, self.0[0], self.0[1], 0); - let (t2, carry) = mac(0, self.0[0], self.0[2], carry); - let (t3, carry) = mac(0, self.0[0], self.0[3], carry); - let (t4, carry) = mac(0, self.0[0], self.0[4], carry); - let (t5, t6) = mac(0, self.0[0], self.0[5], carry); - - let (t3, carry) = mac(t3, self.0[1], self.0[2], 0); - let (t4, carry) = mac(t4, self.0[1], self.0[3], carry); - let (t5, carry) = mac(t5, self.0[1], self.0[4], carry); - let (t6, t7) = mac(t6, self.0[1], self.0[5], carry); - - let (t5, carry) = mac(t5, self.0[2], self.0[3], 0); - let (t6, carry) = mac(t6, self.0[2], self.0[4], carry); - let (t7, t8) = mac(t7, self.0[2], self.0[5], carry); - - let (t7, carry) = mac(t7, self.0[3], self.0[4], 0); - let (t8, t9) = mac(t8, self.0[3], self.0[5], carry); - - let (t9, t10) = mac(t9, self.0[4], self.0[5], 0); - - let t11 = t10 >> 63; - let t10 = (t10 << 1) | (t9 >> 63); - let t9 = (t9 << 1) | (t8 >> 63); - let t8 = (t8 << 1) | (t7 >> 63); - let t7 = (t7 << 1) | (t6 >> 63); - let t6 = (t6 << 1) | (t5 >> 63); - let t5 = (t5 << 1) | (t4 >> 63); - let t4 = (t4 << 1) | (t3 >> 63); - let t3 = (t3 << 1) | (t2 >> 63); - let t2 = (t2 << 1) | (t1 >> 63); - let t1 = t1 << 1; - - let (t0, carry) = mac(0, self.0[0], self.0[0], 0); - let (t1, carry) = adc(t1, 0, carry); - let (t2, carry) = mac(t2, self.0[1], self.0[1], carry); - let (t3, carry) = adc(t3, 0, carry); - let (t4, carry) = mac(t4, self.0[2], self.0[2], carry); - let (t5, carry) = adc(t5, 0, carry); - let (t6, carry) = mac(t6, self.0[3], self.0[3], carry); - let (t7, carry) = adc(t7, 0, carry); - let (t8, carry) = mac(t8, self.0[4], self.0[4], carry); - let (t9, carry) = adc(t9, 0, carry); - let (t10, carry) = mac(t10, self.0[5], self.0[5], carry); - let (t11, _) = adc(t11, 0, carry); - - Self::montgomery_reduce(t0, t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11) - } -} - -#[test] -fn test_conditional_selection() { - let a = Fp([1, 2, 3, 4, 5, 6]); - let b = Fp([7, 8, 9, 10, 11, 12]); - - assert_eq!( - ConditionallySelectable::conditional_select(&a, &b, Choice::from(0u8)), - a - ); - assert_eq!( - ConditionallySelectable::conditional_select(&a, &b, Choice::from(1u8)), - b - ); -} - -#[test] -fn test_equality() { - fn is_equal(a: &Fp, b: &Fp) -> bool { - let eq = a == b; - let ct_eq = a.ct_eq(&b); - - assert_eq!(eq, bool::from(ct_eq)); - - eq - } - - assert!(is_equal(&Fp([1, 2, 3, 4, 5, 6]), &Fp([1, 2, 3, 4, 5, 6]))); - - assert!(!is_equal(&Fp([7, 2, 3, 4, 5, 6]), &Fp([1, 2, 3, 4, 5, 6]))); - assert!(!is_equal(&Fp([1, 7, 3, 4, 5, 6]), &Fp([1, 2, 3, 4, 5, 6]))); - assert!(!is_equal(&Fp([1, 2, 7, 4, 5, 6]), &Fp([1, 2, 3, 4, 5, 6]))); - assert!(!is_equal(&Fp([1, 2, 3, 7, 5, 6]), &Fp([1, 2, 3, 4, 5, 6]))); - assert!(!is_equal(&Fp([1, 2, 3, 4, 7, 6]), &Fp([1, 2, 3, 4, 5, 6]))); - assert!(!is_equal(&Fp([1, 2, 3, 4, 5, 7]), &Fp([1, 2, 3, 4, 5, 6]))); -} - -#[test] -fn test_squaring() { - let a = Fp([ - 0xd215_d276_8e83_191b, - 0x5085_d80f_8fb2_8261, - 0xce9a_032d_df39_3a56, - 0x3e9c_4fff_2ca0_c4bb, - 0x6436_b6f7_f4d9_5dfb, - 0x1060_6628_ad4a_4d90, - ]); - let b = Fp([ - 0x33d9_c42a_3cb3_e235, - 0xdad1_1a09_4c4c_d455, - 0xa2f1_44bd_729a_aeba, - 0xd415_0932_be9f_feac, - 0xe27b_c7c4_7d44_ee50, - 0x14b6_a78d_3ec7_a560, - ]); - - assert_eq!(a.square(), b); -} - -#[test] -fn test_multiplication() { - let a = Fp([ - 0x0397_a383_2017_0cd4, - 0x734c_1b2c_9e76_1d30, - 0x5ed2_55ad_9a48_beb5, - 0x095a_3c6b_22a7_fcfc, - 0x2294_ce75_d4e2_6a27, - 0x1333_8bd8_7001_1ebb, - ]); - let b = Fp([ - 0xb9c3_c7c5_b119_6af7, - 0x2580_e208_6ce3_35c1, - 0xf49a_ed3d_8a57_ef42, - 0x41f2_81e4_9846_e878, - 0xe076_2346_c384_52ce, - 0x0652_e893_26e5_7dc0, - ]); - let c = Fp([ - 0xf96e_f3d7_11ab_5355, - 0xe8d4_59ea_00f1_48dd, - 0x53f7_354a_5f00_fa78, - 0x9e34_a4f3_125c_5f83, - 0x3fbe_0c47_ca74_c19e, - 0x01b0_6a8b_bd4a_dfe4, - ]); - - assert_eq!(a * b, c); -} - -#[test] -fn test_addition() { - let a = Fp([ - 0x5360_bb59_7867_8032, - 0x7dd2_75ae_799e_128e, - 0x5c5b_5071_ce4f_4dcf, - 0xcdb2_1f93_078d_bb3e, - 0xc323_65c5_e73f_474a, - 0x115a_2a54_89ba_be5b, - ]); - let b = Fp([ - 0x9fd2_8773_3d23_dda0, - 0xb16b_f2af_738b_3554, - 0x3e57_a75b_d3cc_6d1d, - 0x900b_c0bd_627f_d6d6, - 0xd319_a080_efb2_45fe, - 0x15fd_caa4_e4bb_2091, - ]); - let c = Fp([ - 0x3934_42cc_b58b_b327, - 0x1092_685f_3bd5_47e3, - 0x3382_252c_ab6a_c4c9, - 0xf946_94cb_7688_7f55, - 0x4b21_5e90_93a5_e071, - 0x0d56_e30f_34f5_f853, - ]); - - assert_eq!(a + b, c); -} - -#[test] -fn test_subtraction() { - let a = Fp([ - 0x5360_bb59_7867_8032, - 0x7dd2_75ae_799e_128e, - 0x5c5b_5071_ce4f_4dcf, - 0xcdb2_1f93_078d_bb3e, - 0xc323_65c5_e73f_474a, - 0x115a_2a54_89ba_be5b, - ]); - let b = Fp([ - 0x9fd2_8773_3d23_dda0, - 0xb16b_f2af_738b_3554, - 0x3e57_a75b_d3cc_6d1d, - 0x900b_c0bd_627f_d6d6, - 0xd319_a080_efb2_45fe, - 0x15fd_caa4_e4bb_2091, - ]); - let c = Fp([ - 0x6d8d_33e6_3b43_4d3d, - 0xeb12_82fd_b766_dd39, - 0x8534_7bb6_f133_d6d5, - 0xa21d_aa5a_9892_f727, - 0x3b25_6cfb_3ad8_ae23, - 0x155d_7199_de7f_8464, - ]); - - assert_eq!(a - b, c); -} - -#[test] -fn test_negation() { - let a = Fp([ - 0x5360_bb59_7867_8032, - 0x7dd2_75ae_799e_128e, - 0x5c5b_5071_ce4f_4dcf, - 0xcdb2_1f93_078d_bb3e, - 0xc323_65c5_e73f_474a, - 0x115a_2a54_89ba_be5b, - ]); - let b = Fp([ - 0x669e_44a6_8798_2a79, - 0xa0d9_8a50_37b5_ed71, - 0x0ad5_822f_2861_a854, - 0x96c5_2bf1_ebf7_5781, - 0x87f8_41f0_5c0c_658c, - 0x08a6_e795_afc5_283e, - ]); - - assert_eq!(-a, b); -} - -#[test] -fn test_debug() { - assert_eq!( - format!( - "{:?}", - Fp([ - 0x5360_bb59_7867_8032, - 0x7dd2_75ae_799e_128e, - 0x5c5b_5071_ce4f_4dcf, - 0xcdb2_1f93_078d_bb3e, - 0xc323_65c5_e73f_474a, - 0x115a_2a54_89ba_be5b, - ]) - ), - "0x104bf052ad3bc99bcb176c24a06a6c3aad4eaf2308fc4d282e106c84a757d061052630515305e59bdddf8111bfdeb704" - ); -} - -#[test] -fn test_from_bytes() { - let mut a = Fp([ - 0xdc90_6d9b_e3f9_5dc8, - 0x8755_caf7_4596_91a1, - 0xcff1_a7f4_e958_3ab3, - 0x9b43_821f_849e_2284, - 0xf575_54f3_a297_4f3f, - 0x085d_bea8_4ed4_7f79, - ]); - - for _ in 0..100 { - a = a.square(); - let tmp = a.to_bytes(); - let b = Fp::from_bytes(&tmp).unwrap(); - - assert_eq!(a, b); - } - - assert_eq!( - -Fp::one(), - Fp::from_bytes(&[ - 26, 1, 17, 234, 57, 127, 230, 154, 75, 27, 167, 182, 67, 75, 172, 215, 100, 119, 75, - 132, 243, 133, 18, 191, 103, 48, 210, 160, 246, 176, 246, 36, 30, 171, 255, 254, 177, - 83, 255, 255, 185, 254, 255, 255, 255, 255, 170, 170 - ]) - .unwrap() - ); - - assert!(bool::from( - Fp::from_bytes(&[ - 27, 1, 17, 234, 57, 127, 230, 154, 75, 27, 167, 182, 67, 75, 172, 215, 100, 119, 75, - 132, 243, 133, 18, 191, 103, 48, 210, 160, 246, 176, 246, 36, 30, 171, 255, 254, 177, - 83, 255, 255, 185, 254, 255, 255, 255, 255, 170, 170 - ]) - .is_none() - )); - - assert!(bool::from(Fp::from_bytes(&[0xff; 48]).is_none())); -} - -#[test] -fn test_sqrt() { - // a = 4 - let a = Fp::from_raw_unchecked([ - 0xaa27_0000_000c_fff3, - 0x53cc_0032_fc34_000a, - 0x478f_e97a_6b0a_807f, - 0xb1d3_7ebe_e6ba_24d7, - 0x8ec9_733b_bf78_ab2f, - 0x09d6_4551_3d83_de7e, - ]); - - assert_eq!( - // sqrt(4) = -2 - -a.sqrt().unwrap(), - // 2 - Fp::from_raw_unchecked([ - 0x3213_0000_0006_554f, - 0xb93c_0018_d6c4_0005, - 0x5760_5e0d_b0dd_bb51, - 0x8b25_6521_ed1f_9bcb, - 0x6cf2_8d79_0162_2c03, - 0x11eb_ab9d_bb81_e28c, - ]) - ); -} - -#[test] -fn test_inversion() { - let a = Fp([ - 0x43b4_3a50_78ac_2076, - 0x1ce0_7630_46f8_962b, - 0x724a_5276_486d_735c, - 0x6f05_c2a6_282d_48fd, - 0x2095_bd5b_b4ca_9331, - 0x03b3_5b38_94b0_f7da, - ]); - let b = Fp([ - 0x69ec_d704_0952_148f, - 0x985c_cc20_2219_0f55, - 0xe19b_ba36_a9ad_2f41, - 0x19bb_16c9_5219_dbd8, - 0x14dc_acfd_fb47_8693, - 0x115f_f58a_fff9_a8e1, - ]); - - assert_eq!(a.invert().unwrap(), b); - assert!(bool::from(Fp::zero().invert().is_none())); -} - -#[test] -fn test_lexicographic_largest() { - assert!(!bool::from(Fp::zero().lexicographically_largest())); - assert!(!bool::from(Fp::one().lexicographically_largest())); - assert!(!bool::from( - Fp::from_raw_unchecked([ - 0xa1fa_ffff_fffe_5557, - 0x995b_fff9_76a3_fffe, - 0x03f4_1d24_d174_ceb4, - 0xf654_7998_c199_5dbd, - 0x778a_468f_507a_6034, - 0x0205_5993_1f7f_8103 - ]) - .lexicographically_largest() - )); - assert!(bool::from( - Fp::from_raw_unchecked([ - 0x1804_0000_0001_5554, - 0x8550_0005_3ab0_0001, - 0x633c_b57c_253c_276f, - 0x6e22_d1ec_31eb_b502, - 0xd391_6126_f2d1_4ca2, - 0x17fb_b857_1a00_6596, - ]) - .lexicographically_largest() - )); - assert!(bool::from( - Fp::from_raw_unchecked([ - 0x43f5_ffff_fffc_aaae, - 0x32b7_fff2_ed47_fffd, - 0x07e8_3a49_a2e9_9d69, - 0xeca8_f331_8332_bb7a, - 0xef14_8d1e_a0f4_c069, - 0x040a_b326_3eff_0206, - ]) - .lexicographically_largest() - )); -} - -#[cfg(feature = "zeroize")] -#[test] -fn test_zeroize() { - use zeroize::Zeroize; - - let mut a = Fp::one(); - a.zeroize(); - assert!(bool::from(a.is_zero())); -} diff --git a/constantine/bls12_381/src/fp12.rs b/constantine/bls12_381/src/fp12.rs deleted file mode 100644 index 26b48043a..000000000 --- a/constantine/bls12_381/src/fp12.rs +++ /dev/null @@ -1,659 +0,0 @@ -use crate::fp::*; -use crate::fp2::*; -use crate::fp6::*; - -use core::fmt; -use core::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}; -use subtle::{Choice, ConditionallySelectable, ConstantTimeEq, CtOption}; - -#[cfg(feature = "pairings")] -use rand_core::RngCore; - -/// This represents an element $c_0 + c_1 w$ of $\mathbb{F}_{p^12} = \mathbb{F}_{p^6} / w^2 - v$. -pub struct Fp12 { - pub c0: Fp6, - pub c1: Fp6, -} - -impl From for Fp12 { - fn from(f: Fp) -> Fp12 { - Fp12 { - c0: Fp6::from(f), - c1: Fp6::zero(), - } - } -} - -impl From for Fp12 { - fn from(f: Fp2) -> Fp12 { - Fp12 { - c0: Fp6::from(f), - c1: Fp6::zero(), - } - } -} - -impl From for Fp12 { - fn from(f: Fp6) -> Fp12 { - Fp12 { - c0: f, - c1: Fp6::zero(), - } - } -} - -impl PartialEq for Fp12 { - fn eq(&self, other: &Fp12) -> bool { - self.ct_eq(other).into() - } -} - -impl Copy for Fp12 {} -impl Clone for Fp12 { - #[inline] - fn clone(&self) -> Self { - *self - } -} - -impl Default for Fp12 { - fn default() -> Self { - Fp12::zero() - } -} - -#[cfg(feature = "zeroize")] -impl zeroize::DefaultIsZeroes for Fp12 {} - -impl fmt::Debug for Fp12 { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{:?} + ({:?})*w", self.c0, self.c1) - } -} - -impl ConditionallySelectable for Fp12 { - #[inline(always)] - fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self { - Fp12 { - c0: Fp6::conditional_select(&a.c0, &b.c0, choice), - c1: Fp6::conditional_select(&a.c1, &b.c1, choice), - } - } -} - -impl ConstantTimeEq for Fp12 { - #[inline(always)] - fn ct_eq(&self, other: &Self) -> Choice { - self.c0.ct_eq(&other.c0) & self.c1.ct_eq(&other.c1) - } -} - -impl Fp12 { - #[inline] - pub fn zero() -> Self { - Fp12 { - c0: Fp6::zero(), - c1: Fp6::zero(), - } - } - - #[inline] - pub fn one() -> Self { - Fp12 { - c0: Fp6::one(), - c1: Fp6::zero(), - } - } - - #[cfg(feature = "pairings")] - pub(crate) fn random(mut rng: impl RngCore) -> Self { - Fp12 { - c0: Fp6::random(&mut rng), - c1: Fp6::random(&mut rng), - } - } - - pub fn mul_by_014(&self, c0: &Fp2, c1: &Fp2, c4: &Fp2) -> Fp12 { - let aa = self.c0.mul_by_01(c0, c1); - let bb = self.c1.mul_by_1(c4); - let o = c1 + c4; - let c1 = self.c1 + self.c0; - let c1 = c1.mul_by_01(c0, &o); - let c1 = c1 - aa - bb; - let c0 = bb; - let c0 = c0.mul_by_nonresidue(); - let c0 = c0 + aa; - - Fp12 { c0, c1 } - } - - #[inline(always)] - pub fn is_zero(&self) -> Choice { - self.c0.is_zero() & self.c1.is_zero() - } - - #[inline(always)] - pub fn conjugate(&self) -> Self { - Fp12 { - c0: self.c0, - c1: -self.c1, - } - } - - /// Raises this element to p. - #[inline(always)] - pub fn frobenius_map(&self) -> Self { - let c0 = self.c0.frobenius_map(); - let c1 = self.c1.frobenius_map(); - - // c1 = c1 * (u + 1)^((p - 1) / 6) - let c1 = c1 - * Fp6::from(Fp2 { - c0: Fp::from_raw_unchecked([ - 0x0708_9552_b319_d465, - 0xc669_5f92_b50a_8313, - 0x97e8_3ccc_d117_228f, - 0xa35b_aeca_b2dc_29ee, - 0x1ce3_93ea_5daa_ce4d, - 0x08f2_220f_b0fb_66eb, - ]), - c1: Fp::from_raw_unchecked([ - 0xb2f6_6aad_4ce5_d646, - 0x5842_a06b_fc49_7cec, - 0xcf48_95d4_2599_d394, - 0xc11b_9cba_40a8_e8d0, - 0x2e38_13cb_e5a0_de89, - 0x110e_efda_8884_7faf, - ]), - }); - - Fp12 { c0, c1 } - } - - #[inline] - pub fn square(&self) -> Self { - let ab = self.c0 * self.c1; - let c0c1 = self.c0 + self.c1; - let c0 = self.c1.mul_by_nonresidue(); - let c0 = c0 + self.c0; - let c0 = c0 * c0c1; - let c0 = c0 - ab; - let c1 = ab + ab; - let c0 = c0 - ab.mul_by_nonresidue(); - - Fp12 { c0, c1 } - } - - pub fn invert(&self) -> CtOption { - (self.c0.square() - self.c1.square().mul_by_nonresidue()) - .invert() - .map(|t| Fp12 { - c0: self.c0 * t, - c1: self.c1 * -t, - }) - } -} - -impl<'a, 'b> Mul<&'b Fp12> for &'a Fp12 { - type Output = Fp12; - - #[inline] - fn mul(self, other: &'b Fp12) -> Self::Output { - let aa = self.c0 * other.c0; - let bb = self.c1 * other.c1; - let o = other.c0 + other.c1; - let c1 = self.c1 + self.c0; - let c1 = c1 * o; - let c1 = c1 - aa; - let c1 = c1 - bb; - let c0 = bb.mul_by_nonresidue(); - let c0 = c0 + aa; - - Fp12 { c0, c1 } - } -} - -impl<'a, 'b> Add<&'b Fp12> for &'a Fp12 { - type Output = Fp12; - - #[inline] - fn add(self, rhs: &'b Fp12) -> Self::Output { - Fp12 { - c0: self.c0 + rhs.c0, - c1: self.c1 + rhs.c1, - } - } -} - -impl<'a> Neg for &'a Fp12 { - type Output = Fp12; - - #[inline] - fn neg(self) -> Self::Output { - Fp12 { - c0: -self.c0, - c1: -self.c1, - } - } -} - -impl Neg for Fp12 { - type Output = Fp12; - - #[inline] - fn neg(self) -> Self::Output { - -&self - } -} - -impl<'a, 'b> Sub<&'b Fp12> for &'a Fp12 { - type Output = Fp12; - - #[inline] - fn sub(self, rhs: &'b Fp12) -> Self::Output { - Fp12 { - c0: self.c0 - rhs.c0, - c1: self.c1 - rhs.c1, - } - } -} - -impl_binops_additive!(Fp12, Fp12); -impl_binops_multiplicative!(Fp12, Fp12); - -#[test] -fn test_arithmetic() { - use crate::fp::*; - use crate::fp2::*; - - let a = Fp12 { - c0: Fp6 { - c0: Fp2 { - c0: Fp::from_raw_unchecked([ - 0x47f9_cb98_b1b8_2d58, - 0x5fe9_11eb_a3aa_1d9d, - 0x96bf_1b5f_4dd8_1db3, - 0x8100_d27c_c925_9f5b, - 0xafa2_0b96_7464_0eab, - 0x09bb_cea7_d8d9_497d, - ]), - c1: Fp::from_raw_unchecked([ - 0x0303_cb98_b166_2daa, - 0xd931_10aa_0a62_1d5a, - 0xbfa9_820c_5be4_a468, - 0x0ba3_643e_cb05_a348, - 0xdc35_34bb_1f1c_25a6, - 0x06c3_05bb_19c0_e1c1, - ]), - }, - c1: Fp2 { - c0: Fp::from_raw_unchecked([ - 0x46f9_cb98_b162_d858, - 0x0be9_109c_f7aa_1d57, - 0xc791_bc55_fece_41d2, - 0xf84c_5770_4e38_5ec2, - 0xcb49_c1d9_c010_e60f, - 0x0acd_b8e1_58bf_e3c8, - ]), - c1: Fp::from_raw_unchecked([ - 0x8aef_cb98_b15f_8306, - 0x3ea1_108f_e4f2_1d54, - 0xcf79_f69f_a1b7_df3b, - 0xe4f5_4aa1_d16b_1a3c, - 0xba5e_4ef8_6105_a679, - 0x0ed8_6c07_97be_e5cf, - ]), - }, - c2: Fp2 { - c0: Fp::from_raw_unchecked([ - 0xcee5_cb98_b15c_2db4, - 0x7159_1082_d23a_1d51, - 0xd762_30e9_44a1_7ca4, - 0xd19e_3dd3_549d_d5b6, - 0xa972_dc17_01fa_66e3, - 0x12e3_1f2d_d6bd_e7d6, - ]), - c1: Fp::from_raw_unchecked([ - 0xad2a_cb98_b173_2d9d, - 0x2cfd_10dd_0696_1d64, - 0x0739_6b86_c6ef_24e8, - 0xbd76_e2fd_b1bf_c820, - 0x6afe_a7f6_de94_d0d5, - 0x1099_4b0c_5744_c040, - ]), - }, - }, - c1: Fp6 { - c0: Fp2 { - c0: Fp::from_raw_unchecked([ - 0x47f9_cb98_b1b8_2d58, - 0x5fe9_11eb_a3aa_1d9d, - 0x96bf_1b5f_4dd8_1db3, - 0x8100_d27c_c925_9f5b, - 0xafa2_0b96_7464_0eab, - 0x09bb_cea7_d8d9_497d, - ]), - c1: Fp::from_raw_unchecked([ - 0x0303_cb98_b166_2daa, - 0xd931_10aa_0a62_1d5a, - 0xbfa9_820c_5be4_a468, - 0x0ba3_643e_cb05_a348, - 0xdc35_34bb_1f1c_25a6, - 0x06c3_05bb_19c0_e1c1, - ]), - }, - c1: Fp2 { - c0: Fp::from_raw_unchecked([ - 0x46f9_cb98_b162_d858, - 0x0be9_109c_f7aa_1d57, - 0xc791_bc55_fece_41d2, - 0xf84c_5770_4e38_5ec2, - 0xcb49_c1d9_c010_e60f, - 0x0acd_b8e1_58bf_e3c8, - ]), - c1: Fp::from_raw_unchecked([ - 0x8aef_cb98_b15f_8306, - 0x3ea1_108f_e4f2_1d54, - 0xcf79_f69f_a1b7_df3b, - 0xe4f5_4aa1_d16b_1a3c, - 0xba5e_4ef8_6105_a679, - 0x0ed8_6c07_97be_e5cf, - ]), - }, - c2: Fp2 { - c0: Fp::from_raw_unchecked([ - 0xcee5_cb98_b15c_2db4, - 0x7159_1082_d23a_1d51, - 0xd762_30e9_44a1_7ca4, - 0xd19e_3dd3_549d_d5b6, - 0xa972_dc17_01fa_66e3, - 0x12e3_1f2d_d6bd_e7d6, - ]), - c1: Fp::from_raw_unchecked([ - 0xad2a_cb98_b173_2d9d, - 0x2cfd_10dd_0696_1d64, - 0x0739_6b86_c6ef_24e8, - 0xbd76_e2fd_b1bf_c820, - 0x6afe_a7f6_de94_d0d5, - 0x1099_4b0c_5744_c040, - ]), - }, - }, - }; - - let b = Fp12 { - c0: Fp6 { - c0: Fp2 { - c0: Fp::from_raw_unchecked([ - 0x47f9_cb98_b1b8_2d58, - 0x5fe9_11eb_a3aa_1d9d, - 0x96bf_1b5f_4dd8_1db3, - 0x8100_d272_c925_9f5b, - 0xafa2_0b96_7464_0eab, - 0x09bb_cea7_d8d9_497d, - ]), - c1: Fp::from_raw_unchecked([ - 0x0303_cb98_b166_2daa, - 0xd931_10aa_0a62_1d5a, - 0xbfa9_820c_5be4_a468, - 0x0ba3_643e_cb05_a348, - 0xdc35_34bb_1f1c_25a6, - 0x06c3_05bb_19c0_e1c1, - ]), - }, - c1: Fp2 { - c0: Fp::from_raw_unchecked([ - 0x46f9_cb98_b162_d858, - 0x0be9_109c_f7aa_1d57, - 0xc791_bc55_fece_41d2, - 0xf84c_5770_4e38_5ec2, - 0xcb49_c1d9_c010_e60f, - 0x0acd_b8e1_58bf_e348, - ]), - c1: Fp::from_raw_unchecked([ - 0x8aef_cb98_b15f_8306, - 0x3ea1_108f_e4f2_1d54, - 0xcf79_f69f_a1b7_df3b, - 0xe4f5_4aa1_d16b_1a3c, - 0xba5e_4ef8_6105_a679, - 0x0ed8_6c07_97be_e5cf, - ]), - }, - c2: Fp2 { - c0: Fp::from_raw_unchecked([ - 0xcee5_cb98_b15c_2db4, - 0x7159_1082_d23a_1d51, - 0xd762_30e9_44a1_7ca4, - 0xd19e_3dd3_549d_d5b6, - 0xa972_dc17_01fa_66e3, - 0x12e3_1f2d_d6bd_e7d6, - ]), - c1: Fp::from_raw_unchecked([ - 0xad2a_cb98_b173_2d9d, - 0x2cfd_10dd_0696_1d64, - 0x0739_6b86_c6ef_24e8, - 0xbd76_e2fd_b1bf_c820, - 0x6afe_a7f6_de94_d0d5, - 0x1099_4b0c_5744_c040, - ]), - }, - }, - c1: Fp6 { - c0: Fp2 { - c0: Fp::from_raw_unchecked([ - 0x47f9_cb98_b1b8_2d58, - 0x5fe9_11eb_a3aa_1d9d, - 0x96bf_1b5f_4dd2_1db3, - 0x8100_d27c_c925_9f5b, - 0xafa2_0b96_7464_0eab, - 0x09bb_cea7_d8d9_497d, - ]), - c1: Fp::from_raw_unchecked([ - 0x0303_cb98_b166_2daa, - 0xd931_10aa_0a62_1d5a, - 0xbfa9_820c_5be4_a468, - 0x0ba3_643e_cb05_a348, - 0xdc35_34bb_1f1c_25a6, - 0x06c3_05bb_19c0_e1c1, - ]), - }, - c1: Fp2 { - c0: Fp::from_raw_unchecked([ - 0x46f9_cb98_b162_d858, - 0x0be9_109c_f7aa_1d57, - 0xc791_bc55_fece_41d2, - 0xf84c_5770_4e38_5ec2, - 0xcb49_c1d9_c010_e60f, - 0x0acd_b8e1_58bf_e3c8, - ]), - c1: Fp::from_raw_unchecked([ - 0x8aef_cb98_b15f_8306, - 0x3ea1_108f_e4f2_1d54, - 0xcf79_f69f_a117_df3b, - 0xe4f5_4aa1_d16b_1a3c, - 0xba5e_4ef8_6105_a679, - 0x0ed8_6c07_97be_e5cf, - ]), - }, - c2: Fp2 { - c0: Fp::from_raw_unchecked([ - 0xcee5_cb98_b15c_2db4, - 0x7159_1082_d23a_1d51, - 0xd762_30e9_44a1_7ca4, - 0xd19e_3dd3_549d_d5b6, - 0xa972_dc17_01fa_66e3, - 0x12e3_1f2d_d6bd_e7d6, - ]), - c1: Fp::from_raw_unchecked([ - 0xad2a_cb98_b173_2d9d, - 0x2cfd_10dd_0696_1d64, - 0x0739_6b86_c6ef_24e8, - 0xbd76_e2fd_b1bf_c820, - 0x6afe_a7f6_de94_d0d5, - 0x1099_4b0c_5744_c040, - ]), - }, - }, - }; - - let c = Fp12 { - c0: Fp6 { - c0: Fp2 { - c0: Fp::from_raw_unchecked([ - 0x47f9_cb98_71b8_2d58, - 0x5fe9_11eb_a3aa_1d9d, - 0x96bf_1b5f_4dd8_1db3, - 0x8100_d27c_c925_9f5b, - 0xafa2_0b96_7464_0eab, - 0x09bb_cea7_d8d9_497d, - ]), - c1: Fp::from_raw_unchecked([ - 0x0303_cb98_b166_2daa, - 0xd931_10aa_0a62_1d5a, - 0xbfa9_820c_5be4_a468, - 0x0ba3_643e_cb05_a348, - 0xdc35_34bb_1f1c_25a6, - 0x06c3_05bb_19c0_e1c1, - ]), - }, - c1: Fp2 { - c0: Fp::from_raw_unchecked([ - 0x46f9_cb98_b162_d858, - 0x0be9_109c_f7aa_1d57, - 0x7791_bc55_fece_41d2, - 0xf84c_5770_4e38_5ec2, - 0xcb49_c1d9_c010_e60f, - 0x0acd_b8e1_58bf_e3c8, - ]), - c1: Fp::from_raw_unchecked([ - 0x8aef_cb98_b15f_8306, - 0x3ea1_108f_e4f2_1d54, - 0xcf79_f69f_a1b7_df3b, - 0xe4f5_4aa1_d16b_133c, - 0xba5e_4ef8_6105_a679, - 0x0ed8_6c07_97be_e5cf, - ]), - }, - c2: Fp2 { - c0: Fp::from_raw_unchecked([ - 0xcee5_cb98_b15c_2db4, - 0x7159_1082_d23a_1d51, - 0xd762_40e9_44a1_7ca4, - 0xd19e_3dd3_549d_d5b6, - 0xa972_dc17_01fa_66e3, - 0x12e3_1f2d_d6bd_e7d6, - ]), - c1: Fp::from_raw_unchecked([ - 0xad2a_cb98_b173_2d9d, - 0x2cfd_10dd_0696_1d64, - 0x0739_6b86_c6ef_24e8, - 0xbd76_e2fd_b1bf_c820, - 0x6afe_a7f6_de94_d0d5, - 0x1099_4b0c_1744_c040, - ]), - }, - }, - c1: Fp6 { - c0: Fp2 { - c0: Fp::from_raw_unchecked([ - 0x47f9_cb98_b1b8_2d58, - 0x5fe9_11eb_a3aa_1d9d, - 0x96bf_1b5f_4dd8_1db3, - 0x8100_d27c_c925_9f5b, - 0xafa2_0b96_7464_0eab, - 0x09bb_cea7_d8d9_497d, - ]), - c1: Fp::from_raw_unchecked([ - 0x0303_cb98_b166_2daa, - 0xd931_10aa_0a62_1d5a, - 0xbfa9_820c_5be4_a468, - 0x0ba3_643e_cb05_a348, - 0xdc35_34bb_1f1c_25a6, - 0x06c3_05bb_19c0_e1c1, - ]), - }, - c1: Fp2 { - c0: Fp::from_raw_unchecked([ - 0x46f9_cb98_b162_d858, - 0x0be9_109c_f7aa_1d57, - 0xc791_bc55_fece_41d2, - 0xf84c_5770_4e38_5ec2, - 0xcb49_c1d3_c010_e60f, - 0x0acd_b8e1_58bf_e3c8, - ]), - c1: Fp::from_raw_unchecked([ - 0x8aef_cb98_b15f_8306, - 0x3ea1_108f_e4f2_1d54, - 0xcf79_f69f_a1b7_df3b, - 0xe4f5_4aa1_d16b_1a3c, - 0xba5e_4ef8_6105_a679, - 0x0ed8_6c07_97be_e5cf, - ]), - }, - c2: Fp2 { - c0: Fp::from_raw_unchecked([ - 0xcee5_cb98_b15c_2db4, - 0x7159_1082_d23a_1d51, - 0xd762_30e9_44a1_7ca4, - 0xd19e_3dd3_549d_d5b6, - 0xa972_dc17_01fa_66e3, - 0x12e3_1f2d_d6bd_e7d6, - ]), - c1: Fp::from_raw_unchecked([ - 0xad2a_cb98_b173_2d9d, - 0x2cfd_10dd_0696_1d64, - 0x0739_6b86_c6ef_24e8, - 0xbd76_e2fd_b1bf_c820, - 0x6afe_a7f6_de94_d0d5, - 0x1099_4b0c_5744_1040, - ]), - }, - }, - }; - - // because a and b and c are similar to each other and - // I was lazy, this is just some arbitrary way to make - // them a little more different - let a = a.square().invert().unwrap().square() + c; - let b = b.square().invert().unwrap().square() + a; - let c = c.square().invert().unwrap().square() + b; - - assert_eq!(a.square(), a * a); - assert_eq!(b.square(), b * b); - assert_eq!(c.square(), c * c); - - assert_eq!((a + b) * c.square(), (c * c * a) + (c * c * b)); - - assert_eq!( - a.invert().unwrap() * b.invert().unwrap(), - (a * b).invert().unwrap() - ); - assert_eq!(a.invert().unwrap() * a, Fp12::one()); - - assert!(a != a.frobenius_map()); - assert_eq!( - a, - a.frobenius_map() - .frobenius_map() - .frobenius_map() - .frobenius_map() - .frobenius_map() - .frobenius_map() - .frobenius_map() - .frobenius_map() - .frobenius_map() - .frobenius_map() - .frobenius_map() - .frobenius_map() - ); -} - -#[cfg(feature = "zeroize")] -#[test] -fn test_zeroize() { - use zeroize::Zeroize; - - let mut a = Fp12::one(); - a.zeroize(); - assert!(bool::from(a.is_zero())); -} diff --git a/constantine/bls12_381/src/fp2.rs b/constantine/bls12_381/src/fp2.rs deleted file mode 100644 index 6d99d639f..000000000 --- a/constantine/bls12_381/src/fp2.rs +++ /dev/null @@ -1,898 +0,0 @@ -//! This module implements arithmetic over the quadratic extension field Fp2. -#![allow(clippy::all)] - -use core::fmt; -use core::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}; -use rand_core::RngCore; -use subtle::{Choice, ConditionallySelectable, ConstantTimeEq, CtOption}; - -use crate::fp::Fp; - -#[derive(Copy, Clone)] -pub struct Fp2 { - pub c0: Fp, - pub c1: Fp, -} - -impl fmt::Debug for Fp2 { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{:?} + {:?}*u", self.c0, self.c1) - } -} - -impl Default for Fp2 { - fn default() -> Self { - Fp2::zero() - } -} - -#[cfg(feature = "zeroize")] -impl zeroize::DefaultIsZeroes for Fp2 {} - -impl From for Fp2 { - fn from(f: Fp) -> Fp2 { - Fp2 { - c0: f, - c1: Fp::zero(), - } - } -} - -impl ConstantTimeEq for Fp2 { - fn ct_eq(&self, other: &Self) -> Choice { - self.c0.ct_eq(&other.c0) & self.c1.ct_eq(&other.c1) - } -} - -impl Eq for Fp2 {} -impl PartialEq for Fp2 { - #[inline] - fn eq(&self, other: &Self) -> bool { - bool::from(self.ct_eq(other)) - } -} - -impl ConditionallySelectable for Fp2 { - fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self { - Fp2 { - c0: Fp::conditional_select(&a.c0, &b.c0, choice), - c1: Fp::conditional_select(&a.c1, &b.c1, choice), - } - } -} - -impl<'a> Neg for &'a Fp2 { - type Output = Fp2; - - #[inline] - fn neg(self) -> Fp2 { - self.neg() - } -} - -impl Neg for Fp2 { - type Output = Fp2; - - #[inline] - fn neg(self) -> Fp2 { - -&self - } -} - -impl<'a, 'b> Sub<&'b Fp2> for &'a Fp2 { - type Output = Fp2; - - #[inline] - fn sub(self, rhs: &'b Fp2) -> Fp2 { - self.sub(rhs) - } -} - -impl<'a, 'b> Add<&'b Fp2> for &'a Fp2 { - type Output = Fp2; - - #[inline] - fn add(self, rhs: &'b Fp2) -> Fp2 { - self.add(rhs) - } -} - -impl<'a, 'b> Mul<&'b Fp2> for &'a Fp2 { - type Output = Fp2; - - #[inline] - fn mul(self, rhs: &'b Fp2) -> Fp2 { - self.mul(rhs) - } -} - -impl_binops_additive!(Fp2, Fp2); -impl_binops_multiplicative!(Fp2, Fp2); - -impl Fp2 { - #[inline] - pub const fn zero() -> Fp2 { - Fp2 { - c0: Fp::zero(), - c1: Fp::zero(), - } - } - - #[inline] - pub const fn one() -> Fp2 { - Fp2 { - c0: Fp::one(), - c1: Fp::zero(), - } - } - - pub fn is_zero(&self) -> Choice { - self.c0.is_zero() & self.c1.is_zero() - } - - pub(crate) fn random(mut rng: impl RngCore) -> Fp2 { - Fp2 { - c0: Fp::random(&mut rng), - c1: Fp::random(&mut rng), - } - } - - /// Raises this element to p. - #[inline(always)] - pub fn frobenius_map(&self) -> Self { - // This is always just a conjugation. If you're curious why, here's - // an article about it: https://alicebob.cryptoland.net/the-frobenius-endomorphism-with-finite-fields/ - self.conjugate() - } - - #[inline(always)] - pub fn conjugate(&self) -> Self { - Fp2 { - c0: self.c0, - c1: -self.c1, - } - } - - #[inline(always)] - pub fn mul_by_nonresidue(&self) -> Fp2 { - // Multiply a + bu by u + 1, getting - // au + a + bu^2 + bu - // and because u^2 = -1, we get - // (a - b) + (a + b)u - - Fp2 { - c0: self.c0 - self.c1, - c1: self.c0 + self.c1, - } - } - - /// Returns whether or not this element is strictly lexicographically - /// larger than its negation. - #[inline] - pub fn lexicographically_largest(&self) -> Choice { - // If this element's c1 coefficient is lexicographically largest - // then it is lexicographically largest. Otherwise, in the event - // the c1 coefficient is zero and the c0 coefficient is - // lexicographically largest, then this element is lexicographically - // largest. - - self.c1.lexicographically_largest() - | (self.c1.is_zero() & self.c0.lexicographically_largest()) - } - - pub const fn square(&self) -> Fp2 { - // Complex squaring: - // - // v0 = c0 * c1 - // c0' = (c0 + c1) * (c0 + \beta*c1) - v0 - \beta * v0 - // c1' = 2 * v0 - // - // In BLS12-381's F_{p^2}, our \beta is -1 so we - // can modify this formula: - // - // c0' = (c0 + c1) * (c0 - c1) - // c1' = 2 * c0 * c1 - - let a = (&self.c0).add(&self.c1); - let b = (&self.c0).sub(&self.c1); - let c = (&self.c0).add(&self.c0); - - Fp2 { - c0: (&a).mul(&b), - c1: (&c).mul(&self.c1), - } - } - - pub fn mul(&self, rhs: &Fp2) -> Fp2 { - // F_{p^2} x F_{p^2} multiplication implemented with operand scanning (schoolbook) - // computes the result as: - // - // aยทb = (a_0 b_0 + a_1 b_1 ฮฒ) + (a_0 b_1 + a_1 b_0)i - // - // In BLS12-381's F_{p^2}, our ฮฒ is -1, so the resulting F_{p^2} element is: - // - // c_0 = a_0 b_0 - a_1 b_1 - // c_1 = a_0 b_1 + a_1 b_0 - // - // Each of these is a "sum of products", which we can compute efficiently. - - Fp2 { - c0: Fp::sum_of_products([self.c0, -self.c1], [rhs.c0, rhs.c1]), - c1: Fp::sum_of_products([self.c0, self.c1], [rhs.c1, rhs.c0]), - } - } - - pub const fn add(&self, rhs: &Fp2) -> Fp2 { - Fp2 { - c0: (&self.c0).add(&rhs.c0), - c1: (&self.c1).add(&rhs.c1), - } - } - - pub const fn sub(&self, rhs: &Fp2) -> Fp2 { - Fp2 { - c0: (&self.c0).sub(&rhs.c0), - c1: (&self.c1).sub(&rhs.c1), - } - } - - pub const fn neg(&self) -> Fp2 { - Fp2 { - c0: (&self.c0).neg(), - c1: (&self.c1).neg(), - } - } - - pub fn sqrt(&self) -> CtOption { - // Algorithm 9, https://eprint.iacr.org/2012/685.pdf - // with constant time modifications. - - CtOption::new(Fp2::zero(), self.is_zero()).or_else(|| { - // a1 = self^((p - 3) / 4) - let a1 = self.pow_vartime(&[ - 0xee7f_bfff_ffff_eaaa, - 0x07aa_ffff_ac54_ffff, - 0xd9cc_34a8_3dac_3d89, - 0xd91d_d2e1_3ce1_44af, - 0x92c6_e9ed_90d2_eb35, - 0x0680_447a_8e5f_f9a6, - ]); - - // alpha = a1^2 * self = self^((p - 3) / 2 + 1) = self^((p - 1) / 2) - let alpha = a1.square() * self; - - // x0 = self^((p + 1) / 4) - let x0 = a1 * self; - - // In the event that alpha = -1, the element is order p - 1 and so - // we're just trying to get the square of an element of the subfield - // Fp. This is given by x0 * u, since u = sqrt(-1). Since the element - // x0 = a + bu has b = 0, the solution is therefore au. - CtOption::new( - Fp2 { - c0: -x0.c1, - c1: x0.c0, - }, - alpha.ct_eq(&(&Fp2::one()).neg()), - ) - // Otherwise, the correct solution is (1 + alpha)^((q - 1) // 2) * x0 - .or_else(|| { - CtOption::new( - (alpha + Fp2::one()).pow_vartime(&[ - 0xdcff_7fff_ffff_d555, - 0x0f55_ffff_58a9_ffff, - 0xb398_6950_7b58_7b12, - 0xb23b_a5c2_79c2_895f, - 0x258d_d3db_21a5_d66b, - 0x0d00_88f5_1cbf_f34d, - ]) * x0, - Choice::from(1), - ) - }) - // Only return the result if it's really the square root (and so - // self is actually quadratic nonresidue) - .and_then(|sqrt| CtOption::new(sqrt, sqrt.square().ct_eq(self))) - }) - } - - /// Computes the multiplicative inverse of this field - /// element, returning None in the case that this element - /// is zero. - pub fn invert(&self) -> CtOption { - // We wish to find the multiplicative inverse of a nonzero - // element a + bu in Fp2. We leverage an identity - // - // (a + bu)(a - bu) = a^2 + b^2 - // - // which holds because u^2 = -1. This can be rewritten as - // - // (a + bu)(a - bu)/(a^2 + b^2) = 1 - // - // because a^2 + b^2 = 0 has no nonzero solutions for (a, b). - // This gives that (a - bu)/(a^2 + b^2) is the inverse - // of (a + bu). Importantly, this can be computing using - // only a single inversion in Fp. - - (self.c0.square() + self.c1.square()).invert().map(|t| Fp2 { - c0: self.c0 * t, - c1: self.c1 * -t, - }) - } - - /// Although this is labeled "vartime", it is only - /// variable time with respect to the exponent. It - /// is also not exposed in the public API. - pub fn pow_vartime(&self, by: &[u64; 6]) -> Self { - let mut res = Self::one(); - for e in by.iter().rev() { - for i in (0..64).rev() { - res = res.square(); - - if ((*e >> i) & 1) == 1 { - res *= self; - } - } - } - res - } - - /// Vartime exponentiation for larger exponents, only - /// used in testing and not exposed through the public API. - #[cfg(all(test, feature = "experimental"))] - pub(crate) fn pow_vartime_extended(&self, by: &[u64]) -> Self { - let mut res = Self::one(); - for e in by.iter().rev() { - for i in (0..64).rev() { - res = res.square(); - - if ((*e >> i) & 1) == 1 { - res *= self; - } - } - } - res - } -} - -#[test] -fn test_conditional_selection() { - let a = Fp2 { - c0: Fp::from_raw_unchecked([1, 2, 3, 4, 5, 6]), - c1: Fp::from_raw_unchecked([7, 8, 9, 10, 11, 12]), - }; - let b = Fp2 { - c0: Fp::from_raw_unchecked([13, 14, 15, 16, 17, 18]), - c1: Fp::from_raw_unchecked([19, 20, 21, 22, 23, 24]), - }; - - assert_eq!( - ConditionallySelectable::conditional_select(&a, &b, Choice::from(0u8)), - a - ); - assert_eq!( - ConditionallySelectable::conditional_select(&a, &b, Choice::from(1u8)), - b - ); -} - -#[test] -fn test_equality() { - fn is_equal(a: &Fp2, b: &Fp2) -> bool { - let eq = a == b; - let ct_eq = a.ct_eq(&b); - - assert_eq!(eq, bool::from(ct_eq)); - - eq - } - - assert!(is_equal( - &Fp2 { - c0: Fp::from_raw_unchecked([1, 2, 3, 4, 5, 6]), - c1: Fp::from_raw_unchecked([7, 8, 9, 10, 11, 12]), - }, - &Fp2 { - c0: Fp::from_raw_unchecked([1, 2, 3, 4, 5, 6]), - c1: Fp::from_raw_unchecked([7, 8, 9, 10, 11, 12]), - } - )); - - assert!(!is_equal( - &Fp2 { - c0: Fp::from_raw_unchecked([2, 2, 3, 4, 5, 6]), - c1: Fp::from_raw_unchecked([7, 8, 9, 10, 11, 12]), - }, - &Fp2 { - c0: Fp::from_raw_unchecked([1, 2, 3, 4, 5, 6]), - c1: Fp::from_raw_unchecked([7, 8, 9, 10, 11, 12]), - } - )); - - assert!(!is_equal( - &Fp2 { - c0: Fp::from_raw_unchecked([1, 2, 3, 4, 5, 6]), - c1: Fp::from_raw_unchecked([2, 8, 9, 10, 11, 12]), - }, - &Fp2 { - c0: Fp::from_raw_unchecked([1, 2, 3, 4, 5, 6]), - c1: Fp::from_raw_unchecked([7, 8, 9, 10, 11, 12]), - } - )); -} - -#[test] -fn test_squaring() { - let a = Fp2 { - c0: Fp::from_raw_unchecked([ - 0xc9a2_1831_63ee_70d4, - 0xbc37_70a7_196b_5c91, - 0xa247_f8c1_304c_5f44, - 0xb01f_c2a3_726c_80b5, - 0xe1d2_93e5_bbd9_19c9, - 0x04b7_8e80_020e_f2ca, - ]), - c1: Fp::from_raw_unchecked([ - 0x952e_a446_0462_618f, - 0x238d_5edd_f025_c62f, - 0xf6c9_4b01_2ea9_2e72, - 0x03ce_24ea_c1c9_3808, - 0x0559_50f9_45da_483c, - 0x010a_768d_0df4_eabc, - ]), - }; - let b = Fp2 { - c0: Fp::from_raw_unchecked([ - 0xa1e0_9175_a4d2_c1fe, - 0x8b33_acfc_204e_ff12, - 0xe244_15a1_1b45_6e42, - 0x61d9_96b1_b6ee_1936, - 0x1164_dbe8_667c_853c, - 0x0788_557a_cc7d_9c79, - ]), - c1: Fp::from_raw_unchecked([ - 0xda6a_87cc_6f48_fa36, - 0x0fc7_b488_277c_1903, - 0x9445_ac4a_dc44_8187, - 0x0261_6d5b_c909_9209, - 0xdbed_4677_2db5_8d48, - 0x11b9_4d50_76c7_b7b1, - ]), - }; - - assert_eq!(a.square(), b); -} - -#[test] -fn test_multiplication() { - let a = Fp2 { - c0: Fp::from_raw_unchecked([ - 0xc9a2_1831_63ee_70d4, - 0xbc37_70a7_196b_5c91, - 0xa247_f8c1_304c_5f44, - 0xb01f_c2a3_726c_80b5, - 0xe1d2_93e5_bbd9_19c9, - 0x04b7_8e80_020e_f2ca, - ]), - c1: Fp::from_raw_unchecked([ - 0x952e_a446_0462_618f, - 0x238d_5edd_f025_c62f, - 0xf6c9_4b01_2ea9_2e72, - 0x03ce_24ea_c1c9_3808, - 0x0559_50f9_45da_483c, - 0x010a_768d_0df4_eabc, - ]), - }; - let b = Fp2 { - c0: Fp::from_raw_unchecked([ - 0xa1e0_9175_a4d2_c1fe, - 0x8b33_acfc_204e_ff12, - 0xe244_15a1_1b45_6e42, - 0x61d9_96b1_b6ee_1936, - 0x1164_dbe8_667c_853c, - 0x0788_557a_cc7d_9c79, - ]), - c1: Fp::from_raw_unchecked([ - 0xda6a_87cc_6f48_fa36, - 0x0fc7_b488_277c_1903, - 0x9445_ac4a_dc44_8187, - 0x0261_6d5b_c909_9209, - 0xdbed_4677_2db5_8d48, - 0x11b9_4d50_76c7_b7b1, - ]), - }; - let c = Fp2 { - c0: Fp::from_raw_unchecked([ - 0xf597_483e_27b4_e0f7, - 0x610f_badf_811d_ae5f, - 0x8432_af91_7714_327a, - 0x6a9a_9603_cf88_f09e, - 0xf05a_7bf8_bad0_eb01, - 0x0954_9131_c003_ffae, - ]), - c1: Fp::from_raw_unchecked([ - 0x963b_02d0_f93d_37cd, - 0xc95c_e1cd_b30a_73d4, - 0x3087_25fa_3126_f9b8, - 0x56da_3c16_7fab_0d50, - 0x6b50_86b5_f4b6_d6af, - 0x09c3_9f06_2f18_e9f2, - ]), - }; - - assert_eq!(a * b, c); -} - -#[test] -fn test_addition() { - let a = Fp2 { - c0: Fp::from_raw_unchecked([ - 0xc9a2_1831_63ee_70d4, - 0xbc37_70a7_196b_5c91, - 0xa247_f8c1_304c_5f44, - 0xb01f_c2a3_726c_80b5, - 0xe1d2_93e5_bbd9_19c9, - 0x04b7_8e80_020e_f2ca, - ]), - c1: Fp::from_raw_unchecked([ - 0x952e_a446_0462_618f, - 0x238d_5edd_f025_c62f, - 0xf6c9_4b01_2ea9_2e72, - 0x03ce_24ea_c1c9_3808, - 0x0559_50f9_45da_483c, - 0x010a_768d_0df4_eabc, - ]), - }; - let b = Fp2 { - c0: Fp::from_raw_unchecked([ - 0xa1e0_9175_a4d2_c1fe, - 0x8b33_acfc_204e_ff12, - 0xe244_15a1_1b45_6e42, - 0x61d9_96b1_b6ee_1936, - 0x1164_dbe8_667c_853c, - 0x0788_557a_cc7d_9c79, - ]), - c1: Fp::from_raw_unchecked([ - 0xda6a_87cc_6f48_fa36, - 0x0fc7_b488_277c_1903, - 0x9445_ac4a_dc44_8187, - 0x0261_6d5b_c909_9209, - 0xdbed_4677_2db5_8d48, - 0x11b9_4d50_76c7_b7b1, - ]), - }; - let c = Fp2 { - c0: Fp::from_raw_unchecked([ - 0x6b82_a9a7_08c1_32d2, - 0x476b_1da3_39ba_5ba4, - 0x848c_0e62_4b91_cd87, - 0x11f9_5955_295a_99ec, - 0xf337_6fce_2255_9f06, - 0x0c3f_e3fa_ce8c_8f43, - ]), - c1: Fp::from_raw_unchecked([ - 0x6f99_2c12_73ab_5bc5, - 0x3355_1366_17a1_df33, - 0x8b0e_f74c_0aed_aff9, - 0x062f_9246_8ad2_ca12, - 0xe146_9770_738f_d584, - 0x12c3_c3dd_84bc_a26d, - ]), - }; - - assert_eq!(a + b, c); -} - -#[test] -fn test_subtraction() { - let a = Fp2 { - c0: Fp::from_raw_unchecked([ - 0xc9a2_1831_63ee_70d4, - 0xbc37_70a7_196b_5c91, - 0xa247_f8c1_304c_5f44, - 0xb01f_c2a3_726c_80b5, - 0xe1d2_93e5_bbd9_19c9, - 0x04b7_8e80_020e_f2ca, - ]), - c1: Fp::from_raw_unchecked([ - 0x952e_a446_0462_618f, - 0x238d_5edd_f025_c62f, - 0xf6c9_4b01_2ea9_2e72, - 0x03ce_24ea_c1c9_3808, - 0x0559_50f9_45da_483c, - 0x010a_768d_0df4_eabc, - ]), - }; - let b = Fp2 { - c0: Fp::from_raw_unchecked([ - 0xa1e0_9175_a4d2_c1fe, - 0x8b33_acfc_204e_ff12, - 0xe244_15a1_1b45_6e42, - 0x61d9_96b1_b6ee_1936, - 0x1164_dbe8_667c_853c, - 0x0788_557a_cc7d_9c79, - ]), - c1: Fp::from_raw_unchecked([ - 0xda6a_87cc_6f48_fa36, - 0x0fc7_b488_277c_1903, - 0x9445_ac4a_dc44_8187, - 0x0261_6d5b_c909_9209, - 0xdbed_4677_2db5_8d48, - 0x11b9_4d50_76c7_b7b1, - ]), - }; - let c = Fp2 { - c0: Fp::from_raw_unchecked([ - 0xe1c0_86bb_bf1b_5981, - 0x4faf_c3a9_aa70_5d7e, - 0x2734_b5c1_0bb7_e726, - 0xb2bd_7776_af03_7a3e, - 0x1b89_5fb3_98a8_4164, - 0x1730_4aef_6f11_3cec, - ]), - c1: Fp::from_raw_unchecked([ - 0x74c3_1c79_9519_1204, - 0x3271_aa54_79fd_ad2b, - 0xc9b4_7157_4915_a30f, - 0x65e4_0313_ec44_b8be, - 0x7487_b238_5b70_67cb, - 0x0952_3b26_d0ad_19a4, - ]), - }; - - assert_eq!(a - b, c); -} - -#[test] -fn test_negation() { - let a = Fp2 { - c0: Fp::from_raw_unchecked([ - 0xc9a2_1831_63ee_70d4, - 0xbc37_70a7_196b_5c91, - 0xa247_f8c1_304c_5f44, - 0xb01f_c2a3_726c_80b5, - 0xe1d2_93e5_bbd9_19c9, - 0x04b7_8e80_020e_f2ca, - ]), - c1: Fp::from_raw_unchecked([ - 0x952e_a446_0462_618f, - 0x238d_5edd_f025_c62f, - 0xf6c9_4b01_2ea9_2e72, - 0x03ce_24ea_c1c9_3808, - 0x0559_50f9_45da_483c, - 0x010a_768d_0df4_eabc, - ]), - }; - let b = Fp2 { - c0: Fp::from_raw_unchecked([ - 0xf05c_e7ce_9c11_39d7, - 0x6274_8f57_97e8_a36d, - 0xc4e8_d9df_c664_96df, - 0xb457_88e1_8118_9209, - 0x6949_13d0_8772_930d, - 0x1549_836a_3770_f3cf, - ]), - c1: Fp::from_raw_unchecked([ - 0x24d0_5bb9_fb9d_491c, - 0xfb1e_a120_c12e_39d0, - 0x7067_879f_c807_c7b1, - 0x60a9_269a_31bb_dab6, - 0x45c2_56bc_fd71_649b, - 0x18f6_9b5d_2b8a_fbde, - ]), - }; - - assert_eq!(-a, b); -} - -#[test] -fn test_sqrt() { - // a = 1488924004771393321054797166853618474668089414631333405711627789629391903630694737978065425271543178763948256226639*u + 784063022264861764559335808165825052288770346101304131934508881646553551234697082295473567906267937225174620141295 - let a = Fp2 { - c0: Fp::from_raw_unchecked([ - 0x2bee_d146_27d7_f9e9, - 0xb661_4e06_660e_5dce, - 0x06c4_cc7c_2f91_d42c, - 0x996d_7847_4b7a_63cc, - 0xebae_bc4c_820d_574e, - 0x1886_5e12_d93f_d845, - ]), - c1: Fp::from_raw_unchecked([ - 0x7d82_8664_baf4_f566, - 0xd17e_6639_96ec_7339, - 0x679e_ad55_cb40_78d0, - 0xfe3b_2260_e001_ec28, - 0x3059_93d0_43d9_1b68, - 0x0626_f03c_0489_b72d, - ]), - }; - - assert_eq!(a.sqrt().unwrap().square(), a); - - // b = 5, which is a generator of the p - 1 order - // multiplicative subgroup - let b = Fp2 { - c0: Fp::from_raw_unchecked([ - 0x6631_0000_0010_5545, - 0x2114_0040_0eec_000d, - 0x3fa7_af30_c820_e316, - 0xc52a_8b8d_6387_695d, - 0x9fb4_e61d_1e83_eac5, - 0x005c_b922_afe8_4dc7, - ]), - c1: Fp::zero(), - }; - - assert_eq!(b.sqrt().unwrap().square(), b); - - // c = 25, which is a generator of the (p - 1) / 2 order - // multiplicative subgroup - let c = Fp2 { - c0: Fp::from_raw_unchecked([ - 0x44f6_0000_0051_ffae, - 0x86b8_0141_9948_0043, - 0xd715_9952_f1f3_794a, - 0x755d_6e3d_fe1f_fc12, - 0xd36c_d6db_5547_e905, - 0x02f8_c8ec_bf18_67bb, - ]), - c1: Fp::zero(), - }; - - assert_eq!(c.sqrt().unwrap().square(), c); - - // 2155129644831861015726826462986972654175647013268275306775721078997042729172900466542651176384766902407257452753362*u + 2796889544896299244102912275102369318775038861758288697415827248356648685135290329705805931514906495247464901062529 - // is nonsquare. - assert!(bool::from( - Fp2 { - c0: Fp::from_raw_unchecked([ - 0xc5fa_1bc8_fd00_d7f6, - 0x3830_ca45_4606_003b, - 0x2b28_7f11_04b1_02da, - 0xa7fb_30f2_8230_f23e, - 0x339c_db9e_e953_dbf0, - 0x0d78_ec51_d989_fc57, - ]), - c1: Fp::from_raw_unchecked([ - 0x27ec_4898_cf87_f613, - 0x9de1_394e_1abb_05a5, - 0x0947_f85d_c170_fc14, - 0x586f_bc69_6b61_14b7, - 0x2b34_75a4_077d_7169, - 0x13e1_c895_cc4b_6c22, - ]) - } - .sqrt() - .is_none() - )); -} - -#[test] -fn test_inversion() { - let a = Fp2 { - c0: Fp::from_raw_unchecked([ - 0x1128_ecad_6754_9455, - 0x9e7a_1cff_3a4e_a1a8, - 0xeb20_8d51_e08b_cf27, - 0xe98a_d408_11f5_fc2b, - 0x736c_3a59_232d_511d, - 0x10ac_d42d_29cf_cbb6, - ]), - c1: Fp::from_raw_unchecked([ - 0xd328_e37c_c2f5_8d41, - 0x948d_f085_8a60_5869, - 0x6032_f9d5_6f93_a573, - 0x2be4_83ef_3fff_dc87, - 0x30ef_61f8_8f48_3c2a, - 0x1333_f55a_3572_5be0, - ]), - }; - - let b = Fp2 { - c0: Fp::from_raw_unchecked([ - 0x0581_a133_3d4f_48a6, - 0x5824_2f6e_f074_8500, - 0x0292_c955_349e_6da5, - 0xba37_721d_dd95_fcd0, - 0x70d1_6790_3aa5_dfc5, - 0x1189_5e11_8b58_a9d5, - ]), - c1: Fp::from_raw_unchecked([ - 0x0eda_09d2_d7a8_5d17, - 0x8808_e137_a7d1_a2cf, - 0x43ae_2625_c1ff_21db, - 0xf85a_c9fd_f7a7_4c64, - 0x8fcc_dda5_b8da_9738, - 0x08e8_4f0c_b32c_d17d, - ]), - }; - - assert_eq!(a.invert().unwrap(), b); - - assert!(bool::from(Fp2::zero().invert().is_none())); -} - -#[test] -fn test_lexicographic_largest() { - assert!(!bool::from(Fp2::zero().lexicographically_largest())); - assert!(!bool::from(Fp2::one().lexicographically_largest())); - assert!(bool::from( - Fp2 { - c0: Fp::from_raw_unchecked([ - 0x1128_ecad_6754_9455, - 0x9e7a_1cff_3a4e_a1a8, - 0xeb20_8d51_e08b_cf27, - 0xe98a_d408_11f5_fc2b, - 0x736c_3a59_232d_511d, - 0x10ac_d42d_29cf_cbb6, - ]), - c1: Fp::from_raw_unchecked([ - 0xd328_e37c_c2f5_8d41, - 0x948d_f085_8a60_5869, - 0x6032_f9d5_6f93_a573, - 0x2be4_83ef_3fff_dc87, - 0x30ef_61f8_8f48_3c2a, - 0x1333_f55a_3572_5be0, - ]), - } - .lexicographically_largest() - )); - assert!(!bool::from( - Fp2 { - c0: -Fp::from_raw_unchecked([ - 0x1128_ecad_6754_9455, - 0x9e7a_1cff_3a4e_a1a8, - 0xeb20_8d51_e08b_cf27, - 0xe98a_d408_11f5_fc2b, - 0x736c_3a59_232d_511d, - 0x10ac_d42d_29cf_cbb6, - ]), - c1: -Fp::from_raw_unchecked([ - 0xd328_e37c_c2f5_8d41, - 0x948d_f085_8a60_5869, - 0x6032_f9d5_6f93_a573, - 0x2be4_83ef_3fff_dc87, - 0x30ef_61f8_8f48_3c2a, - 0x1333_f55a_3572_5be0, - ]), - } - .lexicographically_largest() - )); - assert!(!bool::from( - Fp2 { - c0: Fp::from_raw_unchecked([ - 0x1128_ecad_6754_9455, - 0x9e7a_1cff_3a4e_a1a8, - 0xeb20_8d51_e08b_cf27, - 0xe98a_d408_11f5_fc2b, - 0x736c_3a59_232d_511d, - 0x10ac_d42d_29cf_cbb6, - ]), - c1: Fp::zero(), - } - .lexicographically_largest() - )); - assert!(bool::from( - Fp2 { - c0: -Fp::from_raw_unchecked([ - 0x1128_ecad_6754_9455, - 0x9e7a_1cff_3a4e_a1a8, - 0xeb20_8d51_e08b_cf27, - 0xe98a_d408_11f5_fc2b, - 0x736c_3a59_232d_511d, - 0x10ac_d42d_29cf_cbb6, - ]), - c1: Fp::zero(), - } - .lexicographically_largest() - )); -} - -#[cfg(feature = "zeroize")] -#[test] -fn test_zeroize() { - use zeroize::Zeroize; - - let mut a = Fp2::one(); - a.zeroize(); - assert!(bool::from(a.is_zero())); -} diff --git a/constantine/bls12_381/src/fp6.rs b/constantine/bls12_381/src/fp6.rs deleted file mode 100644 index 554204220..000000000 --- a/constantine/bls12_381/src/fp6.rs +++ /dev/null @@ -1,571 +0,0 @@ -use crate::fp::*; -use crate::fp2::*; - -use core::fmt; -use core::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}; -use subtle::{Choice, ConditionallySelectable, ConstantTimeEq, CtOption}; - -#[cfg(feature = "pairings")] -use rand_core::RngCore; - -/// This represents an element $c_0 + c_1 v + c_2 v^2$ of $\mathbb{F}_{p^6} = \mathbb{F}_{p^2} / v^3 - u - 1$. -pub struct Fp6 { - pub c0: Fp2, - pub c1: Fp2, - pub c2: Fp2, -} - -impl From for Fp6 { - fn from(f: Fp) -> Fp6 { - Fp6 { - c0: Fp2::from(f), - c1: Fp2::zero(), - c2: Fp2::zero(), - } - } -} - -impl From for Fp6 { - fn from(f: Fp2) -> Fp6 { - Fp6 { - c0: f, - c1: Fp2::zero(), - c2: Fp2::zero(), - } - } -} - -impl PartialEq for Fp6 { - fn eq(&self, other: &Fp6) -> bool { - self.ct_eq(other).into() - } -} - -impl Copy for Fp6 {} -impl Clone for Fp6 { - #[inline] - fn clone(&self) -> Self { - *self - } -} - -impl Default for Fp6 { - fn default() -> Self { - Fp6::zero() - } -} - -#[cfg(feature = "zeroize")] -impl zeroize::DefaultIsZeroes for Fp6 {} - -impl fmt::Debug for Fp6 { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{:?} + ({:?})*v + ({:?})*v^2", self.c0, self.c1, self.c2) - } -} - -impl ConditionallySelectable for Fp6 { - #[inline(always)] - fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self { - Fp6 { - c0: Fp2::conditional_select(&a.c0, &b.c0, choice), - c1: Fp2::conditional_select(&a.c1, &b.c1, choice), - c2: Fp2::conditional_select(&a.c2, &b.c2, choice), - } - } -} - -impl ConstantTimeEq for Fp6 { - #[inline(always)] - fn ct_eq(&self, other: &Self) -> Choice { - self.c0.ct_eq(&other.c0) & self.c1.ct_eq(&other.c1) & self.c2.ct_eq(&other.c2) - } -} - -impl Fp6 { - #[inline] - pub fn zero() -> Self { - Fp6 { - c0: Fp2::zero(), - c1: Fp2::zero(), - c2: Fp2::zero(), - } - } - - #[inline] - pub fn one() -> Self { - Fp6 { - c0: Fp2::one(), - c1: Fp2::zero(), - c2: Fp2::zero(), - } - } - - #[cfg(feature = "pairings")] - pub(crate) fn random(mut rng: impl RngCore) -> Self { - Fp6 { - c0: Fp2::random(&mut rng), - c1: Fp2::random(&mut rng), - c2: Fp2::random(&mut rng), - } - } - - pub fn mul_by_1(&self, c1: &Fp2) -> Fp6 { - Fp6 { - c0: (self.c2 * c1).mul_by_nonresidue(), - c1: self.c0 * c1, - c2: self.c1 * c1, - } - } - - pub fn mul_by_01(&self, c0: &Fp2, c1: &Fp2) -> Fp6 { - let a_a = self.c0 * c0; - let b_b = self.c1 * c1; - - let t1 = (self.c2 * c1).mul_by_nonresidue() + a_a; - - let t2 = (c0 + c1) * (self.c0 + self.c1) - a_a - b_b; - - let t3 = self.c2 * c0 + b_b; - - Fp6 { - c0: t1, - c1: t2, - c2: t3, - } - } - - /// Multiply by quadratic nonresidue v. - pub fn mul_by_nonresidue(&self) -> Self { - // Given a + bv + cv^2, this produces - // av + bv^2 + cv^3 - // but because v^3 = u + 1, we have - // c(u + 1) + av + v^2 - - Fp6 { - c0: self.c2.mul_by_nonresidue(), - c1: self.c0, - c2: self.c1, - } - } - - /// Raises this element to p. - #[inline(always)] - pub fn frobenius_map(&self) -> Self { - let c0 = self.c0.frobenius_map(); - let c1 = self.c1.frobenius_map(); - let c2 = self.c2.frobenius_map(); - - // c1 = c1 * (u + 1)^((p - 1) / 3) - let c1 = c1 - * Fp2 { - c0: Fp::zero(), - c1: Fp::from_raw_unchecked([ - 0xcd03_c9e4_8671_f071, - 0x5dab_2246_1fcd_a5d2, - 0x5870_42af_d385_1b95, - 0x8eb6_0ebe_01ba_cb9e, - 0x03f9_7d6e_83d0_50d2, - 0x18f0_2065_5463_8741, - ]), - }; - - // c2 = c2 * (u + 1)^((2p - 2) / 3) - let c2 = c2 - * Fp2 { - c0: Fp::from_raw_unchecked([ - 0x890d_c9e4_8675_45c3, - 0x2af3_2253_3285_a5d5, - 0x5088_0866_309b_7e2c, - 0xa20d_1b8c_7e88_1024, - 0x14e4_f04f_e2db_9068, - 0x14e5_6d3f_1564_853a, - ]), - c1: Fp::zero(), - }; - - Fp6 { c0, c1, c2 } - } - - #[inline(always)] - pub fn is_zero(&self) -> Choice { - self.c0.is_zero() & self.c1.is_zero() & self.c2.is_zero() - } - - /// Returns `c = self * b`. - /// - /// Implements the full-tower interleaving strategy from - /// [ePrint 2022-376](https://eprint.iacr.org/2022/367). - #[inline] - fn mul_interleaved(&self, b: &Self) -> Self { - // The intuition for this algorithm is that we can look at F_p^6 as a direct - // extension of F_p^2, and express the overall operations down to the base field - // F_p instead of only over F_p^2. This enables us to interleave multiplications - // and reductions, ensuring that we don't require double-width intermediate - // representations (with around twice as many limbs as F_p elements). - - // We want to express the multiplication c = a x b, where a = (a_0, a_1, a_2) is - // an element of F_p^6, and a_i = (a_i,0, a_i,1) is an element of F_p^2. The fully - // expanded multiplication is given by (2022-376 ยง5): - // - // c_0,0 = a_0,0 b_0,0 - a_0,1 b_0,1 + a_1,0 b_2,0 - a_1,1 b_2,1 + a_2,0 b_1,0 - a_2,1 b_1,1 - // - a_1,0 b_2,1 - a_1,1 b_2,0 - a_2,0 b_1,1 - a_2,1 b_1,0. - // = a_0,0 b_0,0 - a_0,1 b_0,1 + a_1,0 (b_2,0 - b_2,1) - a_1,1 (b_2,0 + b_2,1) - // + a_2,0 (b_1,0 - b_1,1) - a_2,1 (b_1,0 + b_1,1). - // - // c_0,1 = a_0,0 b_0,1 + a_0,1 b_0,0 + a_1,0 b_2,1 + a_1,1 b_2,0 + a_2,0 b_1,1 + a_2,1 b_1,0 - // + a_1,0 b_2,0 - a_1,1 b_2,1 + a_2,0 b_1,0 - a_2,1 b_1,1. - // = a_0,0 b_0,1 + a_0,1 b_0,0 + a_1,0(b_2,0 + b_2,1) + a_1,1(b_2,0 - b_2,1) - // + a_2,0(b_1,0 + b_1,1) + a_2,1(b_1,0 - b_1,1). - // - // c_1,0 = a_0,0 b_1,0 - a_0,1 b_1,1 + a_1,0 b_0,0 - a_1,1 b_0,1 + a_2,0 b_2,0 - a_2,1 b_2,1 - // - a_2,0 b_2,1 - a_2,1 b_2,0. - // = a_0,0 b_1,0 - a_0,1 b_1,1 + a_1,0 b_0,0 - a_1,1 b_0,1 + a_2,0(b_2,0 - b_2,1) - // - a_2,1(b_2,0 + b_2,1). - // - // c_1,1 = a_0,0 b_1,1 + a_0,1 b_1,0 + a_1,0 b_0,1 + a_1,1 b_0,0 + a_2,0 b_2,1 + a_2,1 b_2,0 - // + a_2,0 b_2,0 - a_2,1 b_2,1 - // = a_0,0 b_1,1 + a_0,1 b_1,0 + a_1,0 b_0,1 + a_1,1 b_0,0 + a_2,0(b_2,0 + b_2,1) - // + a_2,1(b_2,0 - b_2,1). - // - // c_2,0 = a_0,0 b_2,0 - a_0,1 b_2,1 + a_1,0 b_1,0 - a_1,1 b_1,1 + a_2,0 b_0,0 - a_2,1 b_0,1. - // c_2,1 = a_0,0 b_2,1 + a_0,1 b_2,0 + a_1,0 b_1,1 + a_1,1 b_1,0 + a_2,0 b_0,1 + a_2,1 b_0,0. - // - // Each of these is a "sum of products", which we can compute efficiently. - - let a = self; - let b10_p_b11 = b.c1.c0 + b.c1.c1; - let b10_m_b11 = b.c1.c0 - b.c1.c1; - let b20_p_b21 = b.c2.c0 + b.c2.c1; - let b20_m_b21 = b.c2.c0 - b.c2.c1; - - Fp6 { - c0: Fp2 { - c0: Fp::sum_of_products( - [a.c0.c0, -a.c0.c1, a.c1.c0, -a.c1.c1, a.c2.c0, -a.c2.c1], - [b.c0.c0, b.c0.c1, b20_m_b21, b20_p_b21, b10_m_b11, b10_p_b11], - ), - c1: Fp::sum_of_products( - [a.c0.c0, a.c0.c1, a.c1.c0, a.c1.c1, a.c2.c0, a.c2.c1], - [b.c0.c1, b.c0.c0, b20_p_b21, b20_m_b21, b10_p_b11, b10_m_b11], - ), - }, - c1: Fp2 { - c0: Fp::sum_of_products( - [a.c0.c0, -a.c0.c1, a.c1.c0, -a.c1.c1, a.c2.c0, -a.c2.c1], - [b.c1.c0, b.c1.c1, b.c0.c0, b.c0.c1, b20_m_b21, b20_p_b21], - ), - c1: Fp::sum_of_products( - [a.c0.c0, a.c0.c1, a.c1.c0, a.c1.c1, a.c2.c0, a.c2.c1], - [b.c1.c1, b.c1.c0, b.c0.c1, b.c0.c0, b20_p_b21, b20_m_b21], - ), - }, - c2: Fp2 { - c0: Fp::sum_of_products( - [a.c0.c0, -a.c0.c1, a.c1.c0, -a.c1.c1, a.c2.c0, -a.c2.c1], - [b.c2.c0, b.c2.c1, b.c1.c0, b.c1.c1, b.c0.c0, b.c0.c1], - ), - c1: Fp::sum_of_products( - [a.c0.c0, a.c0.c1, a.c1.c0, a.c1.c1, a.c2.c0, a.c2.c1], - [b.c2.c1, b.c2.c0, b.c1.c1, b.c1.c0, b.c0.c1, b.c0.c0], - ), - }, - } - } - - #[inline] - pub fn square(&self) -> Self { - let s0 = self.c0.square(); - let ab = self.c0 * self.c1; - let s1 = ab + ab; - let s2 = (self.c0 - self.c1 + self.c2).square(); - let bc = self.c1 * self.c2; - let s3 = bc + bc; - let s4 = self.c2.square(); - - Fp6 { - c0: s3.mul_by_nonresidue() + s0, - c1: s4.mul_by_nonresidue() + s1, - c2: s1 + s2 + s3 - s0 - s4, - } - } - - #[inline] - pub fn invert(&self) -> CtOption { - let c0 = (self.c1 * self.c2).mul_by_nonresidue(); - let c0 = self.c0.square() - c0; - - let c1 = self.c2.square().mul_by_nonresidue(); - let c1 = c1 - (self.c0 * self.c1); - - let c2 = self.c1.square(); - let c2 = c2 - (self.c0 * self.c2); - - let tmp = ((self.c1 * c2) + (self.c2 * c1)).mul_by_nonresidue(); - let tmp = tmp + (self.c0 * c0); - - tmp.invert().map(|t| Fp6 { - c0: t * c0, - c1: t * c1, - c2: t * c2, - }) - } -} - -impl<'a, 'b> Mul<&'b Fp6> for &'a Fp6 { - type Output = Fp6; - - #[inline] - fn mul(self, other: &'b Fp6) -> Self::Output { - self.mul_interleaved(other) - } -} - -impl<'a, 'b> Add<&'b Fp6> for &'a Fp6 { - type Output = Fp6; - - #[inline] - fn add(self, rhs: &'b Fp6) -> Self::Output { - Fp6 { - c0: self.c0 + rhs.c0, - c1: self.c1 + rhs.c1, - c2: self.c2 + rhs.c2, - } - } -} - -impl<'a> Neg for &'a Fp6 { - type Output = Fp6; - - #[inline] - fn neg(self) -> Self::Output { - Fp6 { - c0: -self.c0, - c1: -self.c1, - c2: -self.c2, - } - } -} - -impl Neg for Fp6 { - type Output = Fp6; - - #[inline] - fn neg(self) -> Self::Output { - -&self - } -} - -impl<'a, 'b> Sub<&'b Fp6> for &'a Fp6 { - type Output = Fp6; - - #[inline] - fn sub(self, rhs: &'b Fp6) -> Self::Output { - Fp6 { - c0: self.c0 - rhs.c0, - c1: self.c1 - rhs.c1, - c2: self.c2 - rhs.c2, - } - } -} - -impl_binops_additive!(Fp6, Fp6); -impl_binops_multiplicative!(Fp6, Fp6); - -#[test] -fn test_arithmetic() { - use crate::fp::*; - - let a = Fp6 { - c0: Fp2 { - c0: Fp::from_raw_unchecked([ - 0x47f9_cb98_b1b8_2d58, - 0x5fe9_11eb_a3aa_1d9d, - 0x96bf_1b5f_4dd8_1db3, - 0x8100_d27c_c925_9f5b, - 0xafa2_0b96_7464_0eab, - 0x09bb_cea7_d8d9_497d, - ]), - c1: Fp::from_raw_unchecked([ - 0x0303_cb98_b166_2daa, - 0xd931_10aa_0a62_1d5a, - 0xbfa9_820c_5be4_a468, - 0x0ba3_643e_cb05_a348, - 0xdc35_34bb_1f1c_25a6, - 0x06c3_05bb_19c0_e1c1, - ]), - }, - c1: Fp2 { - c0: Fp::from_raw_unchecked([ - 0x46f9_cb98_b162_d858, - 0x0be9_109c_f7aa_1d57, - 0xc791_bc55_fece_41d2, - 0xf84c_5770_4e38_5ec2, - 0xcb49_c1d9_c010_e60f, - 0x0acd_b8e1_58bf_e3c8, - ]), - c1: Fp::from_raw_unchecked([ - 0x8aef_cb98_b15f_8306, - 0x3ea1_108f_e4f2_1d54, - 0xcf79_f69f_a1b7_df3b, - 0xe4f5_4aa1_d16b_1a3c, - 0xba5e_4ef8_6105_a679, - 0x0ed8_6c07_97be_e5cf, - ]), - }, - c2: Fp2 { - c0: Fp::from_raw_unchecked([ - 0xcee5_cb98_b15c_2db4, - 0x7159_1082_d23a_1d51, - 0xd762_30e9_44a1_7ca4, - 0xd19e_3dd3_549d_d5b6, - 0xa972_dc17_01fa_66e3, - 0x12e3_1f2d_d6bd_e7d6, - ]), - c1: Fp::from_raw_unchecked([ - 0xad2a_cb98_b173_2d9d, - 0x2cfd_10dd_0696_1d64, - 0x0739_6b86_c6ef_24e8, - 0xbd76_e2fd_b1bf_c820, - 0x6afe_a7f6_de94_d0d5, - 0x1099_4b0c_5744_c040, - ]), - }, - }; - - let b = Fp6 { - c0: Fp2 { - c0: Fp::from_raw_unchecked([ - 0xf120_cb98_b16f_d84b, - 0x5fb5_10cf_f3de_1d61, - 0x0f21_a5d0_69d8_c251, - 0xaa1f_d62f_34f2_839a, - 0x5a13_3515_7f89_913f, - 0x14a3_fe32_9643_c247, - ]), - c1: Fp::from_raw_unchecked([ - 0x3516_cb98_b16c_82f9, - 0x926d_10c2_e126_1d5f, - 0x1709_e01a_0cc2_5fba, - 0x96c8_c960_b825_3f14, - 0x4927_c234_207e_51a9, - 0x18ae_b158_d542_c44e, - ]), - }, - c1: Fp2 { - c0: Fp::from_raw_unchecked([ - 0xbf0d_cb98_b169_82fc, - 0xa679_10b7_1d1a_1d5c, - 0xb7c1_47c2_b8fb_06ff, - 0x1efa_710d_47d2_e7ce, - 0xed20_a79c_7e27_653c, - 0x02b8_5294_dac1_dfba, - ]), - c1: Fp::from_raw_unchecked([ - 0x9d52_cb98_b180_82e5, - 0x621d_1111_5176_1d6f, - 0xe798_8260_3b48_af43, - 0x0ad3_1637_a4f4_da37, - 0xaeac_737c_5ac1_cf2e, - 0x006e_7e73_5b48_b824, - ]), - }, - c2: Fp2 { - c0: Fp::from_raw_unchecked([ - 0xe148_cb98_b17d_2d93, - 0x94d5_1104_3ebe_1d6c, - 0xef80_bca9_de32_4cac, - 0xf77c_0969_2827_95b1, - 0x9dc1_009a_fbb6_8f97, - 0x0479_3199_9a47_ba2b, - ]), - c1: Fp::from_raw_unchecked([ - 0x253e_cb98_b179_d841, - 0xc78d_10f7_2c06_1d6a, - 0xf768_f6f3_811b_ea15, - 0xe424_fc9a_ab5a_512b, - 0x8cd5_8db9_9cab_5001, - 0x0883_e4bf_d946_bc32, - ]), - }, - }; - - let c = Fp6 { - c0: Fp2 { - c0: Fp::from_raw_unchecked([ - 0x6934_cb98_b176_82ef, - 0xfa45_10ea_194e_1d67, - 0xff51_313d_2405_877e, - 0xd0cd_efcc_2e8d_0ca5, - 0x7bea_1ad8_3da0_106b, - 0x0c8e_97e6_1845_be39, - ]), - c1: Fp::from_raw_unchecked([ - 0x4779_cb98_b18d_82d8, - 0xb5e9_1144_4daa_1d7a, - 0x2f28_6bda_a653_2fc2, - 0xbca6_94f6_8bae_ff0f, - 0x3d75_e6b8_1a3a_7a5d, - 0x0a44_c3c4_98cc_96a3, - ]), - }, - c1: Fp2 { - c0: Fp::from_raw_unchecked([ - 0x8b6f_cb98_b18a_2d86, - 0xe8a1_1137_3af2_1d77, - 0x3710_a624_493c_cd2b, - 0xa94f_8828_0ee1_ba89, - 0x2c8a_73d6_bb2f_3ac7, - 0x0e4f_76ea_d7cb_98aa, - ]), - c1: Fp::from_raw_unchecked([ - 0xcf65_cb98_b186_d834, - 0x1b59_112a_283a_1d74, - 0x3ef8_e06d_ec26_6a95, - 0x95f8_7b59_9214_7603, - 0x1b9f_00f5_5c23_fb31, - 0x125a_2a11_16ca_9ab1, - ]), - }, - c2: Fp2 { - c0: Fp::from_raw_unchecked([ - 0x135b_cb98_b183_82e2, - 0x4e11_111d_1582_1d72, - 0x46e1_1ab7_8f10_07fe, - 0x82a1_6e8b_1547_317d, - 0x0ab3_8e13_fd18_bb9b, - 0x1664_dd37_55c9_9cb8, - ]), - c1: Fp::from_raw_unchecked([ - 0xce65_cb98_b131_8334, - 0xc759_0fdb_7c3a_1d2e, - 0x6fcb_8164_9d1c_8eb3, - 0x0d44_004d_1727_356a, - 0x3746_b738_a7d0_d296, - 0x136c_144a_96b1_34fc, - ]), - }, - }; - - assert_eq!(a.square(), a * a); - assert_eq!(b.square(), b * b); - assert_eq!(c.square(), c * c); - - assert_eq!((a + b) * c.square(), (c * c * a) + (c * c * b)); - - assert_eq!( - a.invert().unwrap() * b.invert().unwrap(), - (a * b).invert().unwrap() - ); - assert_eq!(a.invert().unwrap() * a, Fp6::one()); -} - -#[cfg(feature = "zeroize")] -#[test] -fn test_zeroize() { - use zeroize::Zeroize; - - let mut a = Fp6::one(); - a.zeroize(); - assert!(bool::from(a.is_zero())); -} diff --git a/constantine/bls12_381/src/g1.rs b/constantine/bls12_381/src/g1.rs deleted file mode 100644 index bf4676659..000000000 --- a/constantine/bls12_381/src/g1.rs +++ /dev/null @@ -1,1798 +0,0 @@ -//! This module provides an implementation of the $\mathbb{G}_1$ group of BLS12-381. - -use core::borrow::Borrow; -use core::fmt; -use core::iter::Sum; -use core::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}; -use group::{ - prime::{PrimeCurve, PrimeCurveAffine, PrimeGroup}, - Curve, Group, GroupEncoding, UncompressedEncoding, -}; -use rand_core::RngCore; -use subtle::{Choice, ConditionallySelectable, ConstantTimeEq, CtOption}; - -#[cfg(feature = "alloc")] -use group::WnafGroup; - -use crate::fp::Fp; -use crate::Scalar; - -/// 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. -/// -/// Values of `G1Affine` are guaranteed to be in the $q$-order subgroup unless an -/// "unchecked" API was misused. -#[cfg_attr(docsrs, doc(cfg(feature = "groups")))] -#[derive(Copy, Clone, Debug)] -pub struct G1Affine { - pub(crate) x: Fp, - pub(crate) y: Fp, - infinity: Choice, -} - -impl Default for G1Affine { - fn default() -> G1Affine { - G1Affine::identity() - } -} - -#[cfg(feature = "zeroize")] -impl zeroize::DefaultIsZeroes for G1Affine {} - -impl fmt::Display for G1Affine { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{:?}", self) - } -} - -impl<'a> From<&'a G1Projective> for G1Affine { - fn from(p: &'a G1Projective) -> G1Affine { - let zinv = p.z.invert().unwrap_or(Fp::zero()); - let x = p.x * zinv; - let y = p.y * zinv; - - let tmp = G1Affine { - x, - y, - infinity: Choice::from(0u8), - }; - - G1Affine::conditional_select(&tmp, &G1Affine::identity(), zinv.is_zero()) - } -} - -impl From for G1Affine { - fn from(p: G1Projective) -> G1Affine { - G1Affine::from(&p) - } -} - -impl ConstantTimeEq for G1Affine { - fn ct_eq(&self, other: &Self) -> Choice { - // The only cases in which two points are equal are - // 1. infinity is set on both - // 2. infinity is not set on both, and their coordinates are equal - - (self.infinity & other.infinity) - | ((!self.infinity) - & (!other.infinity) - & self.x.ct_eq(&other.x) - & self.y.ct_eq(&other.y)) - } -} - -impl ConditionallySelectable for G1Affine { - fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self { - G1Affine { - x: Fp::conditional_select(&a.x, &b.x, choice), - y: Fp::conditional_select(&a.y, &b.y, choice), - infinity: Choice::conditional_select(&a.infinity, &b.infinity, choice), - } - } -} - -impl Eq for G1Affine {} -impl PartialEq for G1Affine { - #[inline] - fn eq(&self, other: &Self) -> bool { - bool::from(self.ct_eq(other)) - } -} - -impl<'a> Neg for &'a G1Affine { - type Output = G1Affine; - - #[inline] - fn neg(self) -> G1Affine { - G1Affine { - x: self.x, - y: Fp::conditional_select(&-self.y, &Fp::one(), self.infinity), - infinity: self.infinity, - } - } -} - -impl Neg for G1Affine { - type Output = G1Affine; - - #[inline] - fn neg(self) -> G1Affine { - -&self - } -} - -impl<'a, 'b> Add<&'b G1Projective> for &'a G1Affine { - type Output = G1Projective; - - #[inline] - fn add(self, rhs: &'b G1Projective) -> G1Projective { - rhs.add_mixed(self) - } -} - -impl<'a, 'b> Add<&'b G1Affine> for &'a G1Projective { - type Output = G1Projective; - - #[inline] - fn add(self, rhs: &'b G1Affine) -> G1Projective { - self.add_mixed(rhs) - } -} - -impl<'a, 'b> Sub<&'b G1Projective> for &'a G1Affine { - type Output = G1Projective; - - #[inline] - fn sub(self, rhs: &'b G1Projective) -> G1Projective { - self + (-rhs) - } -} - -impl<'a, 'b> Sub<&'b G1Affine> for &'a G1Projective { - type Output = G1Projective; - - #[inline] - fn sub(self, rhs: &'b G1Affine) -> G1Projective { - self + (-rhs) - } -} - -impl Sum for G1Projective -where - T: Borrow, -{ - fn sum(iter: I) -> Self - where - I: Iterator, - { - iter.fold(Self::identity(), |acc, item| acc + item.borrow()) - } -} - -impl_binops_additive!(G1Projective, G1Affine); -impl_binops_additive_specify_output!(G1Affine, G1Projective, G1Projective); - -const B: Fp = Fp::from_raw_unchecked([ - 0xaa27_0000_000c_fff3, - 0x53cc_0032_fc34_000a, - 0x478f_e97a_6b0a_807f, - 0xb1d3_7ebe_e6ba_24d7, - 0x8ec9_733b_bf78_ab2f, - 0x09d6_4551_3d83_de7e, -]); - -impl G1Affine { - /// Returns the identity of the group: the point at infinity. - pub fn identity() -> G1Affine { - G1Affine { - x: Fp::zero(), - y: Fp::one(), - infinity: Choice::from(1u8), - } - } - - /// Returns a fixed generator of the group. See [`notes::design`](notes/design/index.html#fixed-generators) - /// for how this generator is chosen. - pub fn generator() -> G1Affine { - G1Affine { - x: Fp::from_raw_unchecked([ - 0x5cb3_8790_fd53_0c16, - 0x7817_fc67_9976_fff5, - 0x154f_95c7_143b_a1c1, - 0xf0ae_6acd_f3d0_e747, - 0xedce_6ecc_21db_f440, - 0x1201_7741_9e0b_fb75, - ]), - y: Fp::from_raw_unchecked([ - 0xbaac_93d5_0ce7_2271, - 0x8c22_631a_7918_fd8e, - 0xdd59_5f13_5707_25ce, - 0x51ac_5829_5040_5194, - 0x0e1c_8c3f_ad00_59c0, - 0x0bbc_3efc_5008_a26a, - ]), - infinity: Choice::from(0u8), - } - } - - /// Serializes this element into compressed form. See [`notes::serialization`](crate::notes::serialization) - /// for details about how group elements are serialized. - pub fn to_compressed(&self) -> [u8; 48] { - // Strictly speaking, self.x is zero already when self.infinity is true, but - // to guard against implementation mistakes we do not assume this. - let mut res = Fp::conditional_select(&self.x, &Fp::zero(), self.infinity).to_bytes(); - - // This point is in compressed form, so we set the most significant bit. - res[0] |= 1u8 << 7; - - // Is this point at infinity? If so, set the second-most significant bit. - res[0] |= u8::conditional_select(&0u8, &(1u8 << 6), self.infinity); - - // Is the y-coordinate the lexicographically largest of the two associated with the - // x-coordinate? If so, set the third-most significant bit so long as this is not - // the point at infinity. - res[0] |= u8::conditional_select( - &0u8, - &(1u8 << 5), - (!self.infinity) & self.y.lexicographically_largest(), - ); - - res - } - - /// Serializes this element into uncompressed form. See [`notes::serialization`](crate::notes::serialization) - /// for details about how group elements are serialized. - pub fn to_uncompressed(&self) -> [u8; 96] { - let mut res = [0; 96]; - - res[0..48].copy_from_slice( - &Fp::conditional_select(&self.x, &Fp::zero(), self.infinity).to_bytes()[..], - ); - res[48..96].copy_from_slice( - &Fp::conditional_select(&self.y, &Fp::zero(), self.infinity).to_bytes()[..], - ); - - // Is this point at infinity? If so, set the second-most significant bit. - res[0] |= u8::conditional_select(&0u8, &(1u8 << 6), self.infinity); - - res - } - - /// Attempts to deserialize an uncompressed element. See [`notes::serialization`](crate::notes::serialization) - /// for details about how group elements are serialized. - pub fn from_uncompressed(bytes: &[u8; 96]) -> CtOption { - Self::from_uncompressed_unchecked(bytes) - .and_then(|p| CtOption::new(p, p.is_on_curve() & p.is_torsion_free())) - } - - /// Attempts to deserialize an uncompressed element, not checking if the - /// element is on the curve and not checking if it is in the correct subgroup. - /// **This is dangerous to call unless you trust the bytes you are reading; otherwise, - /// API invariants may be broken.** Please consider using `from_uncompressed()` instead. - pub fn from_uncompressed_unchecked(bytes: &[u8; 96]) -> CtOption { - // Obtain the three flags from the start of the byte sequence - let compression_flag_set = Choice::from((bytes[0] >> 7) & 1); - let infinity_flag_set = Choice::from((bytes[0] >> 6) & 1); - let sort_flag_set = Choice::from((bytes[0] >> 5) & 1); - - // Attempt to obtain the x-coordinate - let x = { - let mut tmp = [0; 48]; - tmp.copy_from_slice(&bytes[0..48]); - - // Mask away the flag bits - tmp[0] &= 0b0001_1111; - - Fp::from_bytes(&tmp) - }; - - // Attempt to obtain the y-coordinate - let y = { - let mut tmp = [0; 48]; - tmp.copy_from_slice(&bytes[48..96]); - - Fp::from_bytes(&tmp) - }; - - x.and_then(|x| { - y.and_then(|y| { - // Create a point representing this value - let p = G1Affine::conditional_select( - &G1Affine { - x, - y, - infinity: infinity_flag_set, - }, - &G1Affine::identity(), - infinity_flag_set, - ); - - CtOption::new( - p, - // If the infinity flag is set, the x and y coordinates should have been zero. - ((!infinity_flag_set) | (infinity_flag_set & x.is_zero() & y.is_zero())) & - // The compression flag should not have been set, as this is an uncompressed element - (!compression_flag_set) & - // The sort flag should not have been set, as this is an uncompressed element - (!sort_flag_set), - ) - }) - }) - } - - /// Attempts to deserialize a compressed element. See [`notes::serialization`](crate::notes::serialization) - /// for details about how group elements are serialized. - pub fn from_compressed(bytes: &[u8; 48]) -> CtOption { - // We already know the point is on the curve because this is established - // by the y-coordinate recovery procedure in from_compressed_unchecked(). - - Self::from_compressed_unchecked(bytes).and_then(|p| CtOption::new(p, p.is_torsion_free())) - } - - /// Attempts to deserialize an uncompressed element, not checking if the - /// element is in the correct subgroup. - /// **This is dangerous to call unless you trust the bytes you are reading; otherwise, - /// API invariants may be broken.** Please consider using `from_compressed()` instead. - pub fn from_compressed_unchecked(bytes: &[u8; 48]) -> CtOption { - // Obtain the three flags from the start of the byte sequence - let compression_flag_set = Choice::from((bytes[0] >> 7) & 1); - let infinity_flag_set = Choice::from((bytes[0] >> 6) & 1); - let sort_flag_set = Choice::from((bytes[0] >> 5) & 1); - - // Attempt to obtain the x-coordinate - let x = { - let mut tmp = [0; 48]; - tmp.copy_from_slice(&bytes[0..48]); - - // Mask away the flag bits - tmp[0] &= 0b0001_1111; - - Fp::from_bytes(&tmp) - }; - - x.and_then(|x| { - // If the infinity flag is set, return the value assuming - // the x-coordinate is zero and the sort bit is not set. - // - // Otherwise, return a recovered point (assuming the correct - // y-coordinate can be found) so long as the infinity flag - // was not set. - CtOption::new( - G1Affine::identity(), - infinity_flag_set & // Infinity flag should be set - compression_flag_set & // Compression flag should be set - (!sort_flag_set) & // Sort flag should not be set - x.is_zero(), // The x-coordinate should be zero - ) - .or_else(|| { - // Recover a y-coordinate given x by y = sqrt(x^3 + 4) - ((x.square() * x) + B).sqrt().and_then(|y| { - // Switch to the correct y-coordinate if necessary. - let y = Fp::conditional_select( - &y, - &-y, - y.lexicographically_largest() ^ sort_flag_set, - ); - - CtOption::new( - G1Affine { - x, - y, - infinity: infinity_flag_set, - }, - (!infinity_flag_set) & // Infinity flag should not be set - compression_flag_set, // Compression flag should be set - ) - }) - }) - }) - } - - /// Returns true if this element is the identity (the point at infinity). - #[inline] - pub fn is_identity(&self) -> Choice { - self.infinity - } - - /// Returns true if this point is free of an $h$-torsion component, and so it - /// exists within the $q$-order subgroup $\mathbb{G}_1$. This should always return true - /// unless an "unchecked" API was used. - pub fn is_torsion_free(&self) -> Choice { - // Algorithm from Section 6 of https://eprint.iacr.org/2021/1130 - // Updated proof of correctness in https://eprint.iacr.org/2022/352 - // - // Check that endomorphism_p(P) == -[x^2] P - - let minus_x_squared_times_p = G1Projective::from(self).mul_by_x().mul_by_x().neg(); - let endomorphism_p = endomorphism(self); - minus_x_squared_times_p.ct_eq(&G1Projective::from(endomorphism_p)) - } - - /// Returns true if this point is on the curve. This should always return - /// true unless an "unchecked" API was used. - pub fn is_on_curve(&self) -> Choice { - // y^2 - x^3 ?= 4 - (self.y.square() - (self.x.square() * self.x)).ct_eq(&B) | self.infinity - } -} - -/// A nontrivial third root of unity in Fp -pub const BETA: Fp = Fp::from_raw_unchecked([ - 0x30f1_361b_798a_64e8, - 0xf3b8_ddab_7ece_5a2a, - 0x16a8_ca3a_c615_77f7, - 0xc26a_2ff8_74fd_029b, - 0x3636_b766_6070_1c6e, - 0x051b_a4ab_241b_6160, -]); - -fn endomorphism(p: &G1Affine) -> G1Affine { - // Endomorphism of the points on the curve. - // endomorphism_p(x,y) = (BETA * x, y) - // where BETA is a non-trivial cubic root of unity in Fq. - let mut res = *p; - res.x *= BETA; - res -} - -/// This is an element of $\mathbb{G}_1$ represented in the projective coordinate space. -#[cfg_attr(docsrs, doc(cfg(feature = "groups")))] -#[derive(Copy, Clone, Debug)] -pub struct G1Projective { - pub x: Fp, - pub y: Fp, - pub z: Fp, -} - -impl Default for G1Projective { - fn default() -> G1Projective { - G1Projective::identity() - } -} - -#[cfg(feature = "zeroize")] -impl zeroize::DefaultIsZeroes for G1Projective {} - -impl fmt::Display for G1Projective { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{:?}", self) - } -} - -impl<'a> From<&'a G1Affine> for G1Projective { - fn from(p: &'a G1Affine) -> G1Projective { - G1Projective { - x: p.x, - y: p.y, - z: Fp::conditional_select(&Fp::one(), &Fp::zero(), p.infinity), - } - } -} - -impl From for G1Projective { - fn from(p: G1Affine) -> G1Projective { - G1Projective::from(&p) - } -} - -impl ConstantTimeEq for G1Projective { - fn ct_eq(&self, other: &Self) -> Choice { - // Is (xz, yz, z) equal to (x'z', y'z', z') when converted to affine? - - let x1 = self.x * other.z; - let x2 = other.x * self.z; - - let y1 = self.y * other.z; - let y2 = other.y * self.z; - - let self_is_zero = self.z.is_zero(); - let other_is_zero = other.z.is_zero(); - - (self_is_zero & other_is_zero) // Both point at infinity - | ((!self_is_zero) & (!other_is_zero) & x1.ct_eq(&x2) & y1.ct_eq(&y2)) - // Neither point at infinity, coordinates are the same - } -} - -impl ConditionallySelectable for G1Projective { - fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self { - G1Projective { - x: Fp::conditional_select(&a.x, &b.x, choice), - y: Fp::conditional_select(&a.y, &b.y, choice), - z: Fp::conditional_select(&a.z, &b.z, choice), - } - } -} - -impl Eq for G1Projective {} -impl PartialEq for G1Projective { - #[inline] - fn eq(&self, other: &Self) -> bool { - bool::from(self.ct_eq(other)) - } -} - -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<'a, 'b> Mul<&'b Scalar> for &'a G1Projective { - type Output = G1Projective; - - fn mul(self, other: &'b Scalar) -> Self::Output { - self.multiply(&other.to_bytes()) - } -} - -impl<'a, 'b> Mul<&'b G1Projective> for &'a Scalar { - type Output = G1Projective; - - #[inline] - fn mul(self, rhs: &'b G1Projective) -> Self::Output { - rhs * self - } -} - -impl<'a, 'b> Mul<&'b Scalar> for &'a G1Affine { - type Output = G1Projective; - - fn mul(self, other: &'b Scalar) -> Self::Output { - G1Projective::from(self).multiply(&other.to_bytes()) - } -} - -impl<'a, 'b> Mul<&'b G1Affine> for &'a Scalar { - type Output = G1Projective; - - #[inline] - fn mul(self, rhs: &'b G1Affine) -> Self::Output { - rhs * self - } -} - -impl_binops_additive!(G1Projective, G1Projective); -impl_binops_multiplicative!(G1Projective, Scalar); -impl_binops_multiplicative_mixed!(G1Affine, Scalar, G1Projective); -impl_binops_multiplicative_mixed!(Scalar, G1Affine, G1Projective); -impl_binops_multiplicative_mixed!(Scalar, G1Projective, G1Projective); - -#[inline(always)] -fn mul_by_3b(a: Fp) -> Fp { - let a = a + a; // 2 - let a = a + a; // 4 - a + a + a // 12 -} - -impl G1Projective { - /// Returns the identity of the group: the point at infinity. - /// - pub fn identity() -> G1Projective { - G1Projective { - x: Fp::zero(), - y: Fp::one(), - z: Fp::zero(), - } - } - - /// Returns a fixed generator of the group. See [`notes::design`](notes/design/index.html#fixed-generators) - /// for how this generator is chosen. - pub fn generator() -> G1Projective { - G1Projective { - x: Fp::from_raw_unchecked([ - 0x5cb3_8790_fd53_0c16, - 0x7817_fc67_9976_fff5, - 0x154f_95c7_143b_a1c1, - 0xf0ae_6acd_f3d0_e747, - 0xedce_6ecc_21db_f440, - 0x1201_7741_9e0b_fb75, - ]), - y: Fp::from_raw_unchecked([ - 0xbaac_93d5_0ce7_2271, - 0x8c22_631a_7918_fd8e, - 0xdd59_5f13_5707_25ce, - 0x51ac_5829_5040_5194, - 0x0e1c_8c3f_ad00_59c0, - 0x0bbc_3efc_5008_a26a, - ]), - z: Fp::one(), - } - } - - /// Computes the doubling of this point. - pub fn double(&self) -> G1Projective { - // Algorithm 9, https://eprint.iacr.org/2015/1060.pdf - - let t0 = self.y.square(); - let z3 = t0 + t0; - let z3 = z3 + z3; - let z3 = z3 + z3; - let t1 = self.y * self.z; - let t2 = self.z.square(); - let t2 = mul_by_3b(t2); - let x3 = t2 * z3; - let y3 = t0 + t2; - let z3 = t1 * z3; - let t1 = t2 + t2; - let t2 = t1 + t2; - let t0 = t0 - t2; - let y3 = t0 * y3; - let y3 = x3 + y3; - let t1 = self.x * self.y; - let x3 = t0 * t1; - let x3 = x3 + x3; - - let tmp = G1Projective { - x: x3, - y: y3, - z: z3, - }; - - G1Projective::conditional_select(&tmp, &G1Projective::identity(), self.is_identity()) - } - - /// Adds this point to another point. - pub fn add(&self, rhs: &G1Projective) -> G1Projective { - // Algorithm 7, https://eprint.iacr.org/2015/1060.pdf - - let t0 = self.x * rhs.x; - let t1 = self.y * rhs.y; - let t2 = self.z * rhs.z; - let t3 = self.x + self.y; - let t4 = rhs.x + rhs.y; - let t3 = t3 * t4; - let t4 = t0 + t1; - let t3 = t3 - t4; - let t4 = self.y + self.z; - let x3 = rhs.y + rhs.z; - let t4 = t4 * x3; - let x3 = t1 + t2; - let t4 = t4 - x3; - let x3 = self.x + self.z; - let y3 = rhs.x + rhs.z; - let x3 = x3 * y3; - let y3 = t0 + t2; - let y3 = x3 - y3; - let x3 = t0 + t0; - let t0 = x3 + t0; - let t2 = mul_by_3b(t2); - let z3 = t1 + t2; - let t1 = t1 - t2; - let y3 = mul_by_3b(y3); - let x3 = t4 * y3; - let t2 = t3 * t1; - let x3 = t2 - x3; - let y3 = y3 * t0; - let t1 = t1 * z3; - let y3 = t1 + y3; - let t0 = t0 * t3; - let z3 = z3 * t4; - let z3 = z3 + t0; - - G1Projective { - x: x3, - y: y3, - z: z3, - } - } - - /// Adds this point to another point in the affine model. - pub fn add_mixed(&self, rhs: &G1Affine) -> G1Projective { - // Algorithm 8, https://eprint.iacr.org/2015/1060.pdf - - let t0 = self.x * rhs.x; - let t1 = self.y * rhs.y; - let t3 = rhs.x + rhs.y; - let t4 = self.x + self.y; - let t3 = t3 * t4; - let t4 = t0 + t1; - let t3 = t3 - t4; - let t4 = rhs.y * self.z; - let t4 = t4 + self.y; - let y3 = rhs.x * self.z; - let y3 = y3 + self.x; - let x3 = t0 + t0; - let t0 = x3 + t0; - let t2 = mul_by_3b(self.z); - let z3 = t1 + t2; - let t1 = t1 - t2; - let y3 = mul_by_3b(y3); - let x3 = t4 * y3; - let t2 = t3 * t1; - let x3 = t2 - x3; - let y3 = y3 * t0; - let t1 = t1 * z3; - let y3 = t1 + y3; - let t0 = t0 * t3; - let z3 = z3 * t4; - let z3 = z3 + t0; - - let tmp = G1Projective { - x: x3, - y: y3, - z: z3, - }; - - G1Projective::conditional_select(&tmp, self, rhs.is_identity()) - } - - pub fn random(mut rng: impl RngCore) -> Self { - loop { - let x = Fp::random(&mut rng); - let flip_sign = rng.next_u32() % 2 != 0; - - // Obtain the corresponding y-coordinate given x as y = sqrt(x^3 + 4) - let p = ((x.square() * x) + B).sqrt().map(|y| G1Affine { - x, - y: if flip_sign { -y } else { y }, - infinity: 0.into(), - }); - - if p.is_some().into() { - let p = p.unwrap().to_curve().clear_cofactor(); - - if bool::from(!p.is_identity()) { - return p; - } - } - } - } - fn multiply(&self, by: &[u8; 32]) -> G1Projective { - let mut acc = G1Projective::identity(); - - // This is a simple double-and-add implementation of point - // multiplication, moving from most significant to least - // significant bit of the scalar. - // - // We skip the leading bit because it's always unset for Fq - // elements. - for bit in by - .iter() - .rev() - .flat_map(|byte| (0..8).rev().map(move |i| Choice::from((byte >> i) & 1u8))) - .skip(1) - { - acc = acc.double(); - acc = G1Projective::conditional_select(&acc, &(acc + self), bit); - } - - acc - } - - /// Multiply `self` by `crate::BLS_X`, using double and add. - fn mul_by_x(&self) -> G1Projective { - let mut xself = G1Projective::identity(); - // NOTE: in BLS12-381 we can just skip the first bit. - let mut x = crate::BLS_X >> 1; - let mut tmp = *self; - while x != 0 { - tmp = tmp.double(); - - if x % 2 == 1 { - xself += tmp; - } - x >>= 1; - } - // finally, flip the sign - if crate::BLS_X_IS_NEGATIVE { - xself = -xself; - } - xself - } - - /// Multiplies by $(1 - z)$, where $z$ is the parameter of BLS12-381, which - /// [suffices to clear](https://ia.cr/2019/403) the cofactor and map - /// elliptic curve points to elements of $\mathbb{G}\_1$. - pub fn clear_cofactor(&self) -> G1Projective { - self - self.mul_by_x() - } - - /// Converts a batch of `G1Projective` elements into `G1Affine` elements. This - /// function will panic if `p.len() != q.len()`. - pub fn batch_normalize(p: &[Self], q: &mut [G1Affine]) { - assert_eq!(p.len(), q.len()); - - let mut acc = Fp::one(); - for (p, q) in p.iter().zip(q.iter_mut()) { - // We use the `x` field of `G1Affine` to store the product - // of previous z-coordinates seen. - q.x = acc; - - // We will end up skipping all identities in p - acc = Fp::conditional_select(&(acc * p.z), &acc, p.is_identity()); - } - - // This is the inverse, as all z-coordinates are nonzero and the ones - // that are not are skipped. - acc = acc.invert().unwrap(); - - for (p, q) in p.iter().rev().zip(q.iter_mut().rev()) { - let skip = p.is_identity(); - - // Compute tmp = 1/z - let tmp = q.x * acc; - - // Cancel out z-coordinate in denominator of `acc` - acc = Fp::conditional_select(&(acc * p.z), &acc, skip); - - // Set the coordinates to the correct value - q.x = p.x * tmp; - q.y = p.y * tmp; - q.infinity = Choice::from(0u8); - - *q = G1Affine::conditional_select(q, &G1Affine::identity(), skip); - } - } - - /// Returns true if this element is the identity (the point at infinity). - #[inline] - pub fn is_identity(&self) -> Choice { - self.z.is_zero() - } - - /// Returns true if this point is on the curve. This should always return - /// true unless an "unchecked" API was used. - pub fn is_on_curve(&self) -> Choice { - // Y^2 Z = X^3 + b Z^3 - - (self.y.square() * self.z).ct_eq(&(self.x.square() * self.x + self.z.square() * self.z * B)) - | self.z.is_zero() - } -} - -#[derive(Clone, Copy)] -pub struct G1Compressed([u8; 48]); - -impl fmt::Debug for G1Compressed { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.0[..].fmt(f) - } -} - -impl Default for G1Compressed { - fn default() -> Self { - G1Compressed([0; 48]) - } -} - -#[cfg(feature = "zeroize")] -impl zeroize::DefaultIsZeroes for G1Compressed {} - -impl AsRef<[u8]> for G1Compressed { - fn as_ref(&self) -> &[u8] { - &self.0 - } -} - -impl AsMut<[u8]> for G1Compressed { - fn as_mut(&mut self) -> &mut [u8] { - &mut self.0 - } -} - -impl ConstantTimeEq for G1Compressed { - fn ct_eq(&self, other: &Self) -> Choice { - self.0.ct_eq(&other.0) - } -} - -impl Eq for G1Compressed {} -impl PartialEq for G1Compressed { - #[inline] - fn eq(&self, other: &Self) -> bool { - bool::from(self.ct_eq(other)) - } -} - -#[derive(Clone, Copy)] -pub struct G1Uncompressed([u8; 96]); - -impl fmt::Debug for G1Uncompressed { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.0[..].fmt(f) - } -} - -impl Default for G1Uncompressed { - fn default() -> Self { - G1Uncompressed([0; 96]) - } -} - -#[cfg(feature = "zeroize")] -impl zeroize::DefaultIsZeroes for G1Uncompressed {} - -impl AsRef<[u8]> for G1Uncompressed { - fn as_ref(&self) -> &[u8] { - &self.0 - } -} - -impl AsMut<[u8]> for G1Uncompressed { - fn as_mut(&mut self) -> &mut [u8] { - &mut self.0 - } -} - -impl ConstantTimeEq for G1Uncompressed { - fn ct_eq(&self, other: &Self) -> Choice { - self.0.ct_eq(&other.0) - } -} - -impl Eq for G1Uncompressed {} -impl PartialEq for G1Uncompressed { - #[inline] - fn eq(&self, other: &Self) -> bool { - bool::from(self.ct_eq(other)) - } -} - -impl Group for G1Projective { - type Scalar = Scalar; - - fn random(mut rng: impl RngCore) -> Self { - loop { - let x = Fp::random(&mut rng); - let flip_sign = rng.next_u32() % 2 != 0; - - // Obtain the corresponding y-coordinate given x as y = sqrt(x^3 + 4) - let p = ((x.square() * x) + B).sqrt().map(|y| G1Affine { - x, - y: if flip_sign { -y } else { y }, - infinity: 0.into(), - }); - - if p.is_some().into() { - let p = p.unwrap().to_curve().clear_cofactor(); - - if bool::from(!p.is_identity()) { - return p; - } - } - } - } - - fn identity() -> Self { - Self::identity() - } - - fn generator() -> Self { - Self::generator() - } - - fn is_identity(&self) -> Choice { - self.is_identity() - } - - #[must_use] - fn double(&self) -> Self { - self.double() - } -} - -#[cfg(feature = "alloc")] -impl WnafGroup for G1Projective { - fn recommended_wnaf_for_num_scalars(num_scalars: usize) -> usize { - const RECOMMENDATIONS: [usize; 12] = - [1, 3, 7, 20, 43, 120, 273, 563, 1630, 3128, 7933, 62569]; - - let mut ret = 4; - for r in &RECOMMENDATIONS { - if num_scalars > *r { - ret += 1; - } else { - break; - } - } - - ret - } -} - -impl PrimeGroup for G1Projective {} - -impl Curve for G1Projective { - type AffineRepr = G1Affine; - - fn batch_normalize(p: &[Self], q: &mut [Self::AffineRepr]) { - Self::batch_normalize(p, q); - } - - fn to_affine(&self) -> Self::AffineRepr { - self.into() - } -} - -impl PrimeCurve for G1Projective { - type Affine = G1Affine; -} - -impl PrimeCurveAffine for G1Affine { - type Scalar = Scalar; - type Curve = G1Projective; - - fn identity() -> Self { - Self::identity() - } - - fn generator() -> Self { - Self::generator() - } - - fn is_identity(&self) -> Choice { - self.is_identity() - } - - fn to_curve(&self) -> Self::Curve { - self.into() - } -} - -impl GroupEncoding for G1Projective { - type Repr = G1Compressed; - - fn from_bytes(bytes: &Self::Repr) -> CtOption { - G1Affine::from_bytes(bytes).map(Self::from) - } - - fn from_bytes_unchecked(bytes: &Self::Repr) -> CtOption { - G1Affine::from_bytes_unchecked(bytes).map(Self::from) - } - - fn to_bytes(&self) -> Self::Repr { - G1Affine::from(self).to_bytes() - } -} - -impl GroupEncoding for G1Affine { - type Repr = G1Compressed; - - fn from_bytes(bytes: &Self::Repr) -> CtOption { - Self::from_compressed(&bytes.0) - } - - fn from_bytes_unchecked(bytes: &Self::Repr) -> CtOption { - Self::from_compressed_unchecked(&bytes.0) - } - - fn to_bytes(&self) -> Self::Repr { - G1Compressed(self.to_compressed()) - } -} - -impl UncompressedEncoding for G1Affine { - type Uncompressed = G1Uncompressed; - - fn from_uncompressed(bytes: &Self::Uncompressed) -> CtOption { - Self::from_uncompressed(&bytes.0) - } - - fn from_uncompressed_unchecked(bytes: &Self::Uncompressed) -> CtOption { - Self::from_uncompressed_unchecked(&bytes.0) - } - - fn to_uncompressed(&self) -> Self::Uncompressed { - G1Uncompressed(self.to_uncompressed()) - } -} - -#[test] -fn test_beta() { - assert_eq!( - BETA, - Fp::from_bytes(&[ - 0x00u8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5f, 0x19, 0x67, 0x2f, 0xdf, 0x76, - 0xce, 0x51, 0xba, 0x69, 0xc6, 0x07, 0x6a, 0x0f, 0x77, 0xea, 0xdd, 0xb3, 0xa9, 0x3b, - 0xe6, 0xf8, 0x96, 0x88, 0xde, 0x17, 0xd8, 0x13, 0x62, 0x0a, 0x00, 0x02, 0x2e, 0x01, - 0xff, 0xff, 0xff, 0xfe, 0xff, 0xfe - ]) - .unwrap() - ); - assert_ne!(BETA, Fp::one()); - assert_ne!(BETA * BETA, Fp::one()); - assert_eq!(BETA * BETA * BETA, Fp::one()); -} -#[test] -fn test_is_on_curve() { - assert!(bool::from(G1Affine::identity().is_on_curve())); - assert!(bool::from(G1Affine::generator().is_on_curve())); - assert!(bool::from(G1Projective::identity().is_on_curve())); - assert!(bool::from(G1Projective::generator().is_on_curve())); - - let z = Fp::from_raw_unchecked([ - 0xba7a_fa1f_9a6f_e250, - 0xfa0f_5b59_5eaf_e731, - 0x3bdc_4776_94c3_06e7, - 0x2149_be4b_3949_fa24, - 0x64aa_6e06_49b2_078c, - 0x12b1_08ac_3364_3c3e, - ]); - - let gen = G1Affine::generator(); - let mut test = G1Projective { - x: gen.x * z, - y: gen.y * z, - z, - }; - - assert!(bool::from(test.is_on_curve())); - - test.x = z; - assert!(!bool::from(test.is_on_curve())); -} - -#[test] -#[allow(clippy::eq_op)] -fn test_affine_point_equality() { - let a = G1Affine::generator(); - let b = G1Affine::identity(); - - assert!(a == a); - assert!(b == b); - assert!(a != b); - assert!(b != a); -} - -#[test] -#[allow(clippy::eq_op)] -fn test_projective_point_equality() { - let a = G1Projective::generator(); - let b = G1Projective::identity(); - - assert!(a == a); - assert!(b == b); - assert!(a != b); - assert!(b != a); - - let z = Fp::from_raw_unchecked([ - 0xba7a_fa1f_9a6f_e250, - 0xfa0f_5b59_5eaf_e731, - 0x3bdc_4776_94c3_06e7, - 0x2149_be4b_3949_fa24, - 0x64aa_6e06_49b2_078c, - 0x12b1_08ac_3364_3c3e, - ]); - - let mut c = G1Projective { - x: a.x * z, - y: a.y * z, - z, - }; - assert!(bool::from(c.is_on_curve())); - - assert!(a == c); - assert!(b != c); - assert!(c == a); - assert!(c != b); - - c.y = -c.y; - assert!(bool::from(c.is_on_curve())); - - assert!(a != c); - assert!(b != c); - assert!(c != a); - assert!(c != b); - - c.y = -c.y; - c.x = z; - assert!(!bool::from(c.is_on_curve())); - assert!(a != b); - assert!(a != c); - assert!(b != c); -} - -#[test] -fn test_conditionally_select_affine() { - let a = G1Affine::generator(); - let b = G1Affine::identity(); - - assert_eq!(G1Affine::conditional_select(&a, &b, Choice::from(0u8)), a); - assert_eq!(G1Affine::conditional_select(&a, &b, Choice::from(1u8)), b); -} - -#[test] -fn test_conditionally_select_projective() { - let a = G1Projective::generator(); - let b = G1Projective::identity(); - - assert_eq!( - G1Projective::conditional_select(&a, &b, Choice::from(0u8)), - a - ); - assert_eq!( - G1Projective::conditional_select(&a, &b, Choice::from(1u8)), - b - ); -} - -#[test] -fn test_projective_to_affine() { - let a = G1Projective::generator(); - let b = G1Projective::identity(); - - assert!(bool::from(G1Affine::from(a).is_on_curve())); - assert!(!bool::from(G1Affine::from(a).is_identity())); - assert!(bool::from(G1Affine::from(b).is_on_curve())); - assert!(bool::from(G1Affine::from(b).is_identity())); - - let z = Fp::from_raw_unchecked([ - 0xba7a_fa1f_9a6f_e250, - 0xfa0f_5b59_5eaf_e731, - 0x3bdc_4776_94c3_06e7, - 0x2149_be4b_3949_fa24, - 0x64aa_6e06_49b2_078c, - 0x12b1_08ac_3364_3c3e, - ]); - - let c = G1Projective { - x: a.x * z, - y: a.y * z, - z, - }; - - assert_eq!(G1Affine::from(c), G1Affine::generator()); -} - -#[test] -fn test_affine_to_projective() { - let a = G1Affine::generator(); - let b = G1Affine::identity(); - - assert!(bool::from(G1Projective::from(a).is_on_curve())); - assert!(!bool::from(G1Projective::from(a).is_identity())); - assert!(bool::from(G1Projective::from(b).is_on_curve())); - assert!(bool::from(G1Projective::from(b).is_identity())); -} - -#[test] -fn test_doubling() { - { - let tmp = G1Projective::identity().double(); - assert!(bool::from(tmp.is_identity())); - assert!(bool::from(tmp.is_on_curve())); - } - { - let tmp = G1Projective::generator().double(); - assert!(!bool::from(tmp.is_identity())); - assert!(bool::from(tmp.is_on_curve())); - - assert_eq!( - G1Affine::from(tmp), - G1Affine { - x: Fp::from_raw_unchecked([ - 0x53e9_78ce_58a9_ba3c, - 0x3ea0_583c_4f3d_65f9, - 0x4d20_bb47_f001_2960, - 0xa54c_664a_e5b2_b5d9, - 0x26b5_52a3_9d7e_b21f, - 0x0008_895d_26e6_8785, - ]), - y: Fp::from_raw_unchecked([ - 0x7011_0b32_9829_3940, - 0xda33_c539_3f1f_6afc, - 0xb86e_dfd1_6a5a_a785, - 0xaec6_d1c9_e7b1_c895, - 0x25cf_c2b5_22d1_1720, - 0x0636_1c83_f8d0_9b15, - ]), - infinity: Choice::from(0u8) - } - ); - } -} - -#[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([ - 0xba7a_fa1f_9a6f_e250, - 0xfa0f_5b59_5eaf_e731, - 0x3bdc_4776_94c3_06e7, - 0x2149_be4b_3949_fa24, - 0x64aa_6e06_49b2_078c, - 0x12b1_08ac_3364_3c3e, - ]); - - b = G1Projective { - x: b.x * z, - y: b.y * 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([ - 0xba7a_fa1f_9a6f_e250, - 0xfa0f_5b59_5eaf_e731, - 0x3bdc_4776_94c3_06e7, - 0x2149_be4b_3949_fa24, - 0x64aa_6e06_49b2_078c, - 0x12b1_08ac_3364_3c3e, - ]); - - b = G1Projective { - x: b.x * z, - y: b.y * 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 += 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([ - 0xcd03_c9e4_8671_f071, - 0x5dab_2246_1fcd_a5d2, - 0x5870_42af_d385_1b95, - 0x8eb6_0ebe_01ba_cb9e, - 0x03f9_7d6e_83d0_50d2, - 0x18f0_2065_5463_8741, - ]); - 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([ - 0x29e1_e987_ef68_f2d0, - 0xc5f3_ec53_1db0_3233, - 0xacd6_c4b6_ca19_730f, - 0x18ad_9e82_7bc2_bab7, - 0x46e3_b2c5_785c_c7a9, - 0x07e5_71d4_2d22_ddd6, - ]), - y: Fp::from_raw_unchecked([ - 0x94d1_17a7_e5a5_39e7, - 0x8e17_ef67_3d4b_5d22, - 0x9d74_6aaf_508a_33ea, - 0x8c6d_883d_2516_c9a2, - 0x0bc3_b8d5_fb04_47f7, - 0x07bf_a4c7_210f_4f44, - ]), - z: Fp::one() - }) - ); - assert!(!bool::from(c.is_identity())); - assert!(bool::from(c.is_on_curve())); - } -} - -#[test] -fn test_mixed_addition() { - { - let a = G1Affine::identity(); - let b = G1Projective::identity(); - let c = a + b; - assert!(bool::from(c.is_identity())); - assert!(bool::from(c.is_on_curve())); - } - { - let a = G1Affine::identity(); - let mut b = G1Projective::generator(); - { - let z = Fp::from_raw_unchecked([ - 0xba7a_fa1f_9a6f_e250, - 0xfa0f_5b59_5eaf_e731, - 0x3bdc_4776_94c3_06e7, - 0x2149_be4b_3949_fa24, - 0x64aa_6e06_49b2_078c, - 0x12b1_08ac_3364_3c3e, - ]); - - b = G1Projective { - x: b.x * z, - y: b.y * 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 = G1Affine::identity(); - let mut b = G1Projective::generator(); - { - let z = Fp::from_raw_unchecked([ - 0xba7a_fa1f_9a6f_e250, - 0xfa0f_5b59_5eaf_e731, - 0x3bdc_4776_94c3_06e7, - 0x2149_be4b_3949_fa24, - 0x64aa_6e06_49b2_078c, - 0x12b1_08ac_3364_3c3e, - ]); - - b = G1Projective { - x: b.x * z, - y: b.y * 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 += G1Affine::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([ - 0xcd03_c9e4_8671_f071, - 0x5dab_2246_1fcd_a5d2, - 0x5870_42af_d385_1b95, - 0x8eb6_0ebe_01ba_cb9e, - 0x03f9_7d6e_83d0_50d2, - 0x18f0_2065_5463_8741, - ]); - let beta = beta.square(); - let a = G1Projective::generator().double().double(); - let b = G1Projective { - x: a.x * beta, - y: -a.y, - z: a.z, - }; - let a = G1Affine::from(a); - 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([ - 0x29e1_e987_ef68_f2d0, - 0xc5f3_ec53_1db0_3233, - 0xacd6_c4b6_ca19_730f, - 0x18ad_9e82_7bc2_bab7, - 0x46e3_b2c5_785c_c7a9, - 0x07e5_71d4_2d22_ddd6, - ]), - y: Fp::from_raw_unchecked([ - 0x94d1_17a7_e5a5_39e7, - 0x8e17_ef67_3d4b_5d22, - 0x9d74_6aaf_508a_33ea, - 0x8c6d_883d_2516_c9a2, - 0x0bc3_b8d5_fb04_47f7, - 0x07bf_a4c7_210f_4f44, - ]), - z: Fp::one() - }) - ); - assert!(!bool::from(c.is_identity())); - assert!(bool::from(c.is_on_curve())); - } -} - -#[test] -#[allow(clippy::eq_op)] -fn test_projective_negation_and_subtraction() { - let a = G1Projective::generator().double(); - assert_eq!(a + (-a), G1Projective::identity()); - assert_eq!(a + (-a), a - a); -} - -#[test] -fn test_affine_negation_and_subtraction() { - let a = G1Affine::generator(); - assert_eq!(G1Projective::from(a) + (-a), G1Projective::identity()); - assert_eq!(G1Projective::from(a) + (-a), G1Projective::from(a) - a); -} - -#[test] -fn test_projective_scalar_multiplication() { - let g = G1Projective::generator(); - let a = Scalar::from_raw([ - 0x2b56_8297_a56d_a71c, - 0xd8c3_9ecb_0ef3_75d1, - 0x435c_38da_67bf_bf96, - 0x8088_a050_26b6_59b2, - ]); - let b = Scalar::from_raw([ - 0x785f_dd9b_26ef_8b85, - 0xc997_f258_3769_5c18, - 0x4c8d_bc39_e7b7_56c1, - 0x70d9_b6cc_6d87_df20, - ]); - let c = a * b; - - assert_eq!((g * a) * b, g * c); -} - -#[test] -fn test_affine_scalar_multiplication() { - let g = G1Affine::generator(); - let a = Scalar::from_raw([ - 0x2b56_8297_a56d_a71c, - 0xd8c3_9ecb_0ef3_75d1, - 0x435c_38da_67bf_bf96, - 0x8088_a050_26b6_59b2, - ]); - let b = Scalar::from_raw([ - 0x785f_dd9b_26ef_8b85, - 0xc997_f258_3769_5c18, - 0x4c8d_bc39_e7b7_56c1, - 0x70d9_b6cc_6d87_df20, - ]); - let c = a * b; - - assert_eq!(G1Affine::from(g * a) * b, g * c); -} - -#[test] -fn test_is_torsion_free() { - let a = G1Affine { - x: Fp::from_raw_unchecked([ - 0x0aba_f895_b97e_43c8, - 0xba4c_6432_eb9b_61b0, - 0x1250_6f52_adfe_307f, - 0x7502_8c34_3933_6b72, - 0x8474_4f05_b8e9_bd71, - 0x113d_554f_b095_54f7, - ]), - y: Fp::from_raw_unchecked([ - 0x73e9_0e88_f5cf_01c0, - 0x3700_7b65_dd31_97e2, - 0x5cf9_a199_2f0d_7c78, - 0x4f83_c10b_9eb3_330d, - 0xf6a6_3f6f_07f6_0961, - 0x0c53_b5b9_7e63_4df3, - ]), - infinity: Choice::from(0u8), - }; - assert!(!bool::from(a.is_torsion_free())); - - assert!(bool::from(G1Affine::identity().is_torsion_free())); - assert!(bool::from(G1Affine::generator().is_torsion_free())); -} - -#[test] -fn test_mul_by_x() { - // multiplying by `x` a point in G1 is the same as multiplying by - // the equivalent scalar. - let generator = G1Projective::generator(); - let x = if crate::BLS_X_IS_NEGATIVE { - -Scalar::from(crate::BLS_X) - } else { - Scalar::from(crate::BLS_X) - }; - assert_eq!(generator.mul_by_x(), generator * x); - - let point = G1Projective::generator() * Scalar::from(42); - assert_eq!(point.mul_by_x(), point * x); -} - -#[test] -fn test_clear_cofactor() { - // the generator (and the identity) are always on the curve, - // even after clearing the cofactor - let generator = G1Projective::generator(); - assert!(bool::from(generator.clear_cofactor().is_on_curve())); - let id = G1Projective::identity(); - assert!(bool::from(id.clear_cofactor().is_on_curve())); - - let z = Fp::from_raw_unchecked([ - 0x3d2d1c670671394e, - 0x0ee3a800a2f7c1ca, - 0x270f4f21da2e5050, - 0xe02840a53f1be768, - 0x55debeb597512690, - 0x08bd25353dc8f791, - ]); - - let point = G1Projective { - x: Fp::from_raw_unchecked([ - 0x48af5ff540c817f0, - 0xd73893acaf379d5a, - 0xe6c43584e18e023c, - 0x1eda39c30f188b3e, - 0xf618c6d3ccc0f8d8, - 0x0073542cd671e16c, - ]) * z, - y: Fp::from_raw_unchecked([ - 0x57bf8be79461d0ba, - 0xfc61459cee3547c3, - 0x0d23567df1ef147b, - 0x0ee187bcce1d9b64, - 0xb0c8cfbe9dc8fdc1, - 0x1328661767ef368b, - ]), - z: z.square() * z, - }; - - assert!(bool::from(point.is_on_curve())); - assert!(!bool::from(G1Affine::from(point).is_torsion_free())); - let cleared_point = point.clear_cofactor(); - assert!(bool::from(cleared_point.is_on_curve())); - assert!(bool::from(G1Affine::from(cleared_point).is_torsion_free())); - - // in BLS12-381 the cofactor in G1 can be - // cleared multiplying by (1-x) - let h_eff = Scalar::from(1) + Scalar::from(crate::BLS_X); - assert_eq!(point.clear_cofactor(), point * h_eff); -} - -#[test] -fn test_batch_normalize() { - let a = G1Projective::generator().double(); - let b = a.double(); - let c = b.double(); - - for a_identity in (0..=1).map(|n| n == 1) { - for b_identity in (0..=1).map(|n| n == 1) { - for c_identity in (0..=1).map(|n| n == 1) { - let mut v = [a, b, c]; - if a_identity { - v[0] = G1Projective::identity() - } - if b_identity { - v[1] = G1Projective::identity() - } - if c_identity { - v[2] = G1Projective::identity() - } - - let mut t = [ - G1Affine::identity(), - G1Affine::identity(), - G1Affine::identity(), - ]; - let expected = [ - G1Affine::from(v[0]), - G1Affine::from(v[1]), - G1Affine::from(v[2]), - ]; - - G1Projective::batch_normalize(&v[..], &mut t[..]); - - assert_eq!(&t[..], &expected[..]); - } - } - } -} - -#[cfg(feature = "zeroize")] -#[test] -fn test_zeroize() { - use zeroize::Zeroize; - - let mut a = G1Affine::generator(); - a.zeroize(); - assert!(bool::from(a.is_identity())); - - let mut a = G1Projective::generator(); - a.zeroize(); - assert!(bool::from(a.is_identity())); - - let mut a = GroupEncoding::to_bytes(&G1Affine::generator()); - a.zeroize(); - assert_eq!(&a, &G1Compressed::default()); - - let mut a = UncompressedEncoding::to_uncompressed(&G1Affine::generator()); - a.zeroize(); - assert_eq!(&a, &G1Uncompressed::default()); -} - -#[test] -fn test_commutative_scalar_subgroup_multiplication() { - let a = Scalar::from_raw([ - 0x1fff_3231_233f_fffd, - 0x4884_b7fa_0003_4802, - 0x998c_4fef_ecbc_4ff3, - 0x1824_b159_acc5_0562, - ]); - - let g1_a = G1Affine::generator(); - let g1_p = G1Projective::generator(); - - // By reference. - assert_eq!(&g1_a * &a, &a * &g1_a); - assert_eq!(&g1_p * &a, &a * &g1_p); - - // Mixed - assert_eq!(&g1_a * a.clone(), a.clone() * &g1_a); - assert_eq!(&g1_p * a.clone(), a.clone() * &g1_p); - assert_eq!(g1_a.clone() * &a, &a * g1_a.clone()); - assert_eq!(g1_p.clone() * &a, &a * g1_p.clone()); - - // By value. - assert_eq!(g1_p * a, a * g1_p); - assert_eq!(g1_a * a, a * g1_a); -} diff --git a/constantine/bls12_381/src/g2.rs b/constantine/bls12_381/src/g2.rs deleted file mode 100644 index 86eafcbc6..000000000 --- a/constantine/bls12_381/src/g2.rs +++ /dev/null @@ -1,2180 +0,0 @@ -//! This module provides an implementation of the $\mathbb{G}_2$ group of BLS12-381. -#![allow(clippy::all)] - -use core::borrow::Borrow; -use core::fmt; -use core::iter::Sum; -use core::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}; -use group::{ - prime::{PrimeCurve, PrimeCurveAffine, PrimeGroup}, - Curve, Group, GroupEncoding, UncompressedEncoding, -}; -use rand_core::RngCore; -use subtle::{Choice, ConditionallySelectable, ConstantTimeEq, CtOption}; - -#[cfg(feature = "alloc")] -use group::WnafGroup; - -use crate::fp::Fp; -use crate::fp2::Fp2; -use crate::Scalar; - -/// 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 -/// improve performance through the use of mixed curve model arithmetic. -/// -/// Values of `G2Affine` are guaranteed to be in the $q$-order subgroup unless an -/// "unchecked" API was misused. -#[cfg_attr(docsrs, doc(cfg(feature = "groups")))] -#[derive(Copy, Clone, Debug)] -pub struct G2Affine { - pub(crate) x: Fp2, - pub(crate) y: Fp2, - infinity: Choice, -} - -impl Default for G2Affine { - fn default() -> G2Affine { - G2Affine::identity() - } -} - -#[cfg(feature = "zeroize")] -impl zeroize::DefaultIsZeroes for G2Affine {} - -impl fmt::Display for G2Affine { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{:?}", self) - } -} - -impl<'a> From<&'a G2Projective> for G2Affine { - fn from(p: &'a G2Projective) -> G2Affine { - let zinv = p.z.invert().unwrap_or(Fp2::zero()); - let x = p.x * zinv; - let y = p.y * zinv; - - let tmp = G2Affine { - x, - y, - infinity: Choice::from(0u8), - }; - - G2Affine::conditional_select(&tmp, &G2Affine::identity(), zinv.is_zero()) - } -} - -impl From for G2Affine { - fn from(p: G2Projective) -> G2Affine { - G2Affine::from(&p) - } -} - -impl ConstantTimeEq for G2Affine { - fn ct_eq(&self, other: &Self) -> Choice { - // The only cases in which two points are equal are - // 1. infinity is set on both - // 2. infinity is not set on both, and their coordinates are equal - - (self.infinity & other.infinity) - | ((!self.infinity) - & (!other.infinity) - & self.x.ct_eq(&other.x) - & self.y.ct_eq(&other.y)) - } -} - -impl ConditionallySelectable for G2Affine { - fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self { - G2Affine { - x: Fp2::conditional_select(&a.x, &b.x, choice), - y: Fp2::conditional_select(&a.y, &b.y, choice), - infinity: Choice::conditional_select(&a.infinity, &b.infinity, choice), - } - } -} - -impl Eq for G2Affine {} -impl PartialEq for G2Affine { - #[inline] - fn eq(&self, other: &Self) -> bool { - bool::from(self.ct_eq(other)) - } -} - -impl<'a> Neg for &'a G2Affine { - type Output = G2Affine; - - #[inline] - fn neg(self) -> G2Affine { - G2Affine { - x: self.x, - y: Fp2::conditional_select(&-self.y, &Fp2::one(), self.infinity), - infinity: self.infinity, - } - } -} - -impl Neg for G2Affine { - type Output = G2Affine; - - #[inline] - fn neg(self) -> G2Affine { - -&self - } -} - -impl<'a, 'b> Add<&'b G2Projective> for &'a G2Affine { - type Output = G2Projective; - - #[inline] - fn add(self, rhs: &'b G2Projective) -> G2Projective { - rhs.add_mixed(self) - } -} - -impl<'a, 'b> Add<&'b G2Affine> for &'a G2Projective { - type Output = G2Projective; - - #[inline] - fn add(self, rhs: &'b G2Affine) -> G2Projective { - self.add_mixed(rhs) - } -} - -impl<'a, 'b> Sub<&'b G2Projective> for &'a G2Affine { - type Output = G2Projective; - - #[inline] - fn sub(self, rhs: &'b G2Projective) -> G2Projective { - self + (-rhs) - } -} - -impl<'a, 'b> Sub<&'b G2Affine> for &'a G2Projective { - type Output = G2Projective; - - #[inline] - fn sub(self, rhs: &'b G2Affine) -> G2Projective { - self + (-rhs) - } -} - -impl Sum for G2Projective -where - T: Borrow, -{ - fn sum(iter: I) -> Self - where - I: Iterator, - { - iter.fold(Self::identity(), |acc, item| acc + item.borrow()) - } -} - -impl_binops_additive!(G2Projective, G2Affine); -impl_binops_additive_specify_output!(G2Affine, G2Projective, G2Projective); - -const B: Fp2 = Fp2 { - c0: Fp::from_raw_unchecked([ - 0xaa27_0000_000c_fff3, - 0x53cc_0032_fc34_000a, - 0x478f_e97a_6b0a_807f, - 0xb1d3_7ebe_e6ba_24d7, - 0x8ec9_733b_bf78_ab2f, - 0x09d6_4551_3d83_de7e, - ]), - c1: Fp::from_raw_unchecked([ - 0xaa27_0000_000c_fff3, - 0x53cc_0032_fc34_000a, - 0x478f_e97a_6b0a_807f, - 0xb1d3_7ebe_e6ba_24d7, - 0x8ec9_733b_bf78_ab2f, - 0x09d6_4551_3d83_de7e, - ]), -}; - -const B3: Fp2 = Fp2::add(&Fp2::add(&B, &B), &B); - -impl G2Affine { - /// Returns the identity of the group: the point at infinity. - pub fn identity() -> G2Affine { - G2Affine { - x: Fp2::zero(), - y: Fp2::one(), - infinity: Choice::from(1u8), - } - } - - /// Returns a fixed generator of the group. See [`notes::design`](notes/design/index.html#fixed-generators) - /// for how this generator is chosen. - pub fn generator() -> G2Affine { - G2Affine { - x: Fp2 { - c0: Fp::from_raw_unchecked([ - 0xf5f2_8fa2_0294_0a10, - 0xb3f5_fb26_87b4_961a, - 0xa1a8_93b5_3e2a_e580, - 0x9894_999d_1a3c_aee9, - 0x6f67_b763_1863_366b, - 0x0581_9192_4350_bcd7, - ]), - c1: Fp::from_raw_unchecked([ - 0xa5a9_c075_9e23_f606, - 0xaaa0_c59d_bccd_60c3, - 0x3bb1_7e18_e286_7806, - 0x1b1a_b6cc_8541_b367, - 0xc2b6_ed0e_f215_8547, - 0x1192_2a09_7360_edf3, - ]), - }, - y: Fp2 { - c0: Fp::from_raw_unchecked([ - 0x4c73_0af8_6049_4c4a, - 0x597c_fa1f_5e36_9c5a, - 0xe7e6_856c_aa0a_635a, - 0xbbef_b5e9_6e0d_495f, - 0x07d3_a975_f0ef_25a2, - 0x0083_fd8e_7e80_dae5, - ]), - c1: Fp::from_raw_unchecked([ - 0xadc0_fc92_df64_b05d, - 0x18aa_270a_2b14_61dc, - 0x86ad_ac6a_3be4_eba0, - 0x7949_5c4e_c93d_a33a, - 0xe717_5850_a43c_caed, - 0x0b2b_c2a1_63de_1bf2, - ]), - }, - infinity: Choice::from(0u8), - } - } - - /// Serializes this element into compressed form. See [`notes::serialization`](crate::notes::serialization) - /// for details about how group elements are serialized. - pub fn to_compressed(&self) -> [u8; 96] { - // Strictly speaking, self.x is zero already when self.infinity is true, but - // to guard against implementation mistakes we do not assume this. - let x = Fp2::conditional_select(&self.x, &Fp2::zero(), self.infinity); - - let mut res = [0; 96]; - - (&mut res[0..48]).copy_from_slice(&x.c1.to_bytes()[..]); - (&mut res[48..96]).copy_from_slice(&x.c0.to_bytes()[..]); - - // This point is in compressed form, so we set the most significant bit. - res[0] |= 1u8 << 7; - - // Is this point at infinity? If so, set the second-most significant bit. - res[0] |= u8::conditional_select(&0u8, &(1u8 << 6), self.infinity); - - // Is the y-coordinate the lexicographically largest of the two associated with the - // x-coordinate? If so, set the third-most significant bit so long as this is not - // the point at infinity. - res[0] |= u8::conditional_select( - &0u8, - &(1u8 << 5), - (!self.infinity) & self.y.lexicographically_largest(), - ); - - res - } - - /// Serializes this element into uncompressed form. See [`notes::serialization`](crate::notes::serialization) - /// for details about how group elements are serialized. - pub fn to_uncompressed(&self) -> [u8; 192] { - let mut res = [0; 192]; - - let x = Fp2::conditional_select(&self.x, &Fp2::zero(), self.infinity); - let y = Fp2::conditional_select(&self.y, &Fp2::zero(), self.infinity); - - res[0..48].copy_from_slice(&x.c1.to_bytes()[..]); - res[48..96].copy_from_slice(&x.c0.to_bytes()[..]); - res[96..144].copy_from_slice(&y.c1.to_bytes()[..]); - res[144..192].copy_from_slice(&y.c0.to_bytes()[..]); - - // Is this point at infinity? If so, set the second-most significant bit. - res[0] |= u8::conditional_select(&0u8, &(1u8 << 6), self.infinity); - - res - } - - /// Attempts to deserialize an uncompressed element. See [`notes::serialization`](crate::notes::serialization) - /// for details about how group elements are serialized. - pub fn from_uncompressed(bytes: &[u8; 192]) -> CtOption { - Self::from_uncompressed_unchecked(bytes) - .and_then(|p| CtOption::new(p, p.is_on_curve() & p.is_torsion_free())) - } - - /// Attempts to deserialize an uncompressed element, not checking if the - /// element is on the curve and not checking if it is in the correct subgroup. - /// **This is dangerous to call unless you trust the bytes you are reading; otherwise, - /// API invariants may be broken.** Please consider using `from_uncompressed()` instead. - pub fn from_uncompressed_unchecked(bytes: &[u8; 192]) -> CtOption { - // Obtain the three flags from the start of the byte sequence - let compression_flag_set = Choice::from((bytes[0] >> 7) & 1); - let infinity_flag_set = Choice::from((bytes[0] >> 6) & 1); - let sort_flag_set = Choice::from((bytes[0] >> 5) & 1); - - // Attempt to obtain the x-coordinate - let xc1 = { - let mut tmp = [0; 48]; - tmp.copy_from_slice(&bytes[0..48]); - - // Mask away the flag bits - tmp[0] &= 0b0001_1111; - - Fp::from_bytes(&tmp) - }; - let xc0 = { - let mut tmp = [0; 48]; - tmp.copy_from_slice(&bytes[48..96]); - - Fp::from_bytes(&tmp) - }; - - // Attempt to obtain the y-coordinate - let yc1 = { - let mut tmp = [0; 48]; - tmp.copy_from_slice(&bytes[96..144]); - - Fp::from_bytes(&tmp) - }; - let yc0 = { - let mut tmp = [0; 48]; - tmp.copy_from_slice(&bytes[144..192]); - - Fp::from_bytes(&tmp) - }; - - xc1.and_then(|xc1| { - xc0.and_then(|xc0| { - yc1.and_then(|yc1| { - yc0.and_then(|yc0| { - let x = Fp2 { - c0: xc0, - c1: xc1 - }; - let y = Fp2 { - c0: yc0, - c1: yc1 - }; - - // Create a point representing this value - let p = G2Affine::conditional_select( - &G2Affine { - x, - y, - infinity: infinity_flag_set, - }, - &G2Affine::identity(), - infinity_flag_set, - ); - - CtOption::new( - p, - // If the infinity flag is set, the x and y coordinates should have been zero. - ((!infinity_flag_set) | (infinity_flag_set & x.is_zero() & y.is_zero())) & - // The compression flag should not have been set, as this is an uncompressed element - (!compression_flag_set) & - // The sort flag should not have been set, as this is an uncompressed element - (!sort_flag_set), - ) - }) - }) - }) - }) - } - - /// Attempts to deserialize a compressed element. See [`notes::serialization`](crate::notes::serialization) - /// for details about how group elements are serialized. - pub fn from_compressed(bytes: &[u8; 96]) -> CtOption { - // We already know the point is on the curve because this is established - // by the y-coordinate recovery procedure in from_compressed_unchecked(). - - Self::from_compressed_unchecked(bytes).and_then(|p| CtOption::new(p, p.is_torsion_free())) - } - - /// Attempts to deserialize an uncompressed element, not checking if the - /// element is in the correct subgroup. - /// **This is dangerous to call unless you trust the bytes you are reading; otherwise, - /// API invariants may be broken.** Please consider using `from_compressed()` instead. - pub fn from_compressed_unchecked(bytes: &[u8; 96]) -> CtOption { - // Obtain the three flags from the start of the byte sequence - let compression_flag_set = Choice::from((bytes[0] >> 7) & 1); - let infinity_flag_set = Choice::from((bytes[0] >> 6) & 1); - let sort_flag_set = Choice::from((bytes[0] >> 5) & 1); - - // Attempt to obtain the x-coordinate - let xc1 = { - let mut tmp = [0; 48]; - tmp.copy_from_slice(&bytes[0..48]); - - // Mask away the flag bits - tmp[0] &= 0b0001_1111; - - Fp::from_bytes(&tmp) - }; - let xc0 = { - let mut tmp = [0; 48]; - tmp.copy_from_slice(&bytes[48..96]); - - Fp::from_bytes(&tmp) - }; - - xc1.and_then(|xc1| { - xc0.and_then(|xc0| { - let x = Fp2 { c0: xc0, c1: xc1 }; - - // If the infinity flag is set, return the value assuming - // the x-coordinate is zero and the sort bit is not set. - // - // Otherwise, return a recovered point (assuming the correct - // y-coordinate can be found) so long as the infinity flag - // was not set. - CtOption::new( - G2Affine::identity(), - infinity_flag_set & // Infinity flag should be set - compression_flag_set & // Compression flag should be set - (!sort_flag_set) & // Sort flag should not be set - x.is_zero(), // The x-coordinate should be zero - ) - .or_else(|| { - // Recover a y-coordinate given x by y = sqrt(x^3 + 4) - ((x.square() * x) + B).sqrt().and_then(|y| { - // Switch to the correct y-coordinate if necessary. - let y = Fp2::conditional_select( - &y, - &-y, - y.lexicographically_largest() ^ sort_flag_set, - ); - - CtOption::new( - G2Affine { - x, - y, - infinity: infinity_flag_set, - }, - (!infinity_flag_set) & // Infinity flag should not be set - compression_flag_set, // Compression flag should be set - ) - }) - }) - }) - }) - } - - /// Returns true if this element is the identity (the point at infinity). - #[inline] - pub fn is_identity(&self) -> Choice { - self.infinity - } - - /// Returns true if this point is free of an $h$-torsion component, and so it - /// exists within the $q$-order subgroup $\mathbb{G}_2$. This should always return true - /// unless an "unchecked" API was used. - pub fn is_torsion_free(&self) -> Choice { - // Algorithm from Section 4 of https://eprint.iacr.org/2021/1130 - // Updated proof of correctness in https://eprint.iacr.org/2022/352 - // - // Check that psi(P) == [x] P - let p = G2Projective::from(self); - p.psi().ct_eq(&p.mul_by_x()) - } - - /// Returns true if this point is on the curve. This should always return - /// true unless an "unchecked" API was used. - pub fn is_on_curve(&self) -> Choice { - // y^2 - x^3 ?= 4(u + 1) - (self.y.square() - (self.x.square() * self.x)).ct_eq(&B) | self.infinity - } -} - -/// This is an element of $\mathbb{G}_2$ represented in the projective coordinate space. -#[cfg_attr(docsrs, doc(cfg(feature = "groups")))] -#[derive(Copy, Clone, Debug)] -pub struct G2Projective { - pub x: Fp2, - pub y: Fp2, - pub z: Fp2, -} - -impl Default for G2Projective { - fn default() -> G2Projective { - G2Projective::identity() - } -} - -#[cfg(feature = "zeroize")] -impl zeroize::DefaultIsZeroes for G2Projective {} - -impl fmt::Display for G2Projective { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{:?}", self) - } -} - -impl<'a> From<&'a G2Affine> for G2Projective { - fn from(p: &'a G2Affine) -> G2Projective { - G2Projective { - x: p.x, - y: p.y, - z: Fp2::conditional_select(&Fp2::one(), &Fp2::zero(), p.infinity), - } - } -} - -impl From for G2Projective { - fn from(p: G2Affine) -> G2Projective { - G2Projective::from(&p) - } -} - -impl ConstantTimeEq for G2Projective { - fn ct_eq(&self, other: &Self) -> Choice { - // Is (xz, yz, z) equal to (x'z', y'z', z') when converted to affine? - - let x1 = self.x * other.z; - let x2 = other.x * self.z; - - let y1 = self.y * other.z; - let y2 = other.y * self.z; - - let self_is_zero = self.z.is_zero(); - let other_is_zero = other.z.is_zero(); - - (self_is_zero & other_is_zero) // Both point at infinity - | ((!self_is_zero) & (!other_is_zero) & x1.ct_eq(&x2) & y1.ct_eq(&y2)) - // Neither point at infinity, coordinates are the same - } -} - -impl ConditionallySelectable for G2Projective { - fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self { - G2Projective { - x: Fp2::conditional_select(&a.x, &b.x, choice), - y: Fp2::conditional_select(&a.y, &b.y, choice), - z: Fp2::conditional_select(&a.z, &b.z, choice), - } - } -} - -impl Eq for G2Projective {} -impl PartialEq for G2Projective { - #[inline] - fn eq(&self, other: &Self) -> bool { - bool::from(self.ct_eq(other)) - } -} - -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<'a, 'b> Mul<&'b Scalar> for &'a G2Projective { - type Output = G2Projective; - - fn mul(self, other: &'b Scalar) -> Self::Output { - self.multiply(&other.to_bytes()) - } -} - -impl<'a, 'b> Mul<&'b G2Projective> for &'a Scalar { - type Output = G2Projective; - - #[inline] - fn mul(self, rhs: &'b G2Projective) -> Self::Output { - rhs * self - } -} - -impl<'a, 'b> Mul<&'b Scalar> for &'a G2Affine { - type Output = G2Projective; - - fn mul(self, other: &'b Scalar) -> Self::Output { - G2Projective::from(self).multiply(&other.to_bytes()) - } -} - -impl<'a, 'b> Mul<&'b G2Affine> for &'a Scalar { - type Output = G2Projective; - - #[inline] - fn mul(self, rhs: &'b G2Affine) -> Self::Output { - rhs * self - } -} - -impl_binops_additive!(G2Projective, G2Projective); -impl_binops_multiplicative!(G2Projective, Scalar); -impl_binops_multiplicative_mixed!(G2Affine, Scalar, G2Projective); -impl_binops_multiplicative_mixed!(Scalar, G2Affine, G2Projective); -impl_binops_multiplicative_mixed!(Scalar, G2Projective, G2Projective); - -#[inline(always)] -fn mul_by_3b(x: Fp2) -> Fp2 { - x * B3 -} - -impl G2Projective { - /// Returns the identity of the group: the point at infinity. - pub fn identity() -> G2Projective { - G2Projective { - x: Fp2::zero(), - y: Fp2::one(), - z: Fp2::zero(), - } - } - - /// Returns a fixed generator of the group. See [`notes::design`](notes/design/index.html#fixed-generators) - /// for how this generator is chosen. - pub fn generator() -> G2Projective { - G2Projective { - x: Fp2 { - c0: Fp::from_raw_unchecked([ - 0xf5f2_8fa2_0294_0a10, - 0xb3f5_fb26_87b4_961a, - 0xa1a8_93b5_3e2a_e580, - 0x9894_999d_1a3c_aee9, - 0x6f67_b763_1863_366b, - 0x0581_9192_4350_bcd7, - ]), - c1: Fp::from_raw_unchecked([ - 0xa5a9_c075_9e23_f606, - 0xaaa0_c59d_bccd_60c3, - 0x3bb1_7e18_e286_7806, - 0x1b1a_b6cc_8541_b367, - 0xc2b6_ed0e_f215_8547, - 0x1192_2a09_7360_edf3, - ]), - }, - y: Fp2 { - c0: Fp::from_raw_unchecked([ - 0x4c73_0af8_6049_4c4a, - 0x597c_fa1f_5e36_9c5a, - 0xe7e6_856c_aa0a_635a, - 0xbbef_b5e9_6e0d_495f, - 0x07d3_a975_f0ef_25a2, - 0x0083_fd8e_7e80_dae5, - ]), - c1: Fp::from_raw_unchecked([ - 0xadc0_fc92_df64_b05d, - 0x18aa_270a_2b14_61dc, - 0x86ad_ac6a_3be4_eba0, - 0x7949_5c4e_c93d_a33a, - 0xe717_5850_a43c_caed, - 0x0b2b_c2a1_63de_1bf2, - ]), - }, - z: Fp2::one(), - } - } - - /// Computes the doubling of this point. - pub fn double(&self) -> G2Projective { - // Algorithm 9, https://eprint.iacr.org/2015/1060.pdf - - let t0 = self.y.square(); - let z3 = t0 + t0; - let z3 = z3 + z3; - let z3 = z3 + z3; - let t1 = self.y * self.z; - let t2 = self.z.square(); - let t2 = mul_by_3b(t2); - let x3 = t2 * z3; - let y3 = t0 + t2; - let z3 = t1 * z3; - let t1 = t2 + t2; - let t2 = t1 + t2; - let t0 = t0 - t2; - let y3 = t0 * y3; - let y3 = x3 + y3; - let t1 = self.x * self.y; - let x3 = t0 * t1; - let x3 = x3 + x3; - - let tmp = G2Projective { - x: x3, - y: y3, - z: z3, - }; - - G2Projective::conditional_select(&tmp, &G2Projective::identity(), self.is_identity()) - } - - /// Adds this point to another point. - pub fn add(&self, rhs: &G2Projective) -> G2Projective { - // Algorithm 7, https://eprint.iacr.org/2015/1060.pdf - - let t0 = self.x * rhs.x; - let t1 = self.y * rhs.y; - let t2 = self.z * rhs.z; - let t3 = self.x + self.y; - let t4 = rhs.x + rhs.y; - let t3 = t3 * t4; - let t4 = t0 + t1; - let t3 = t3 - t4; - let t4 = self.y + self.z; - let x3 = rhs.y + rhs.z; - let t4 = t4 * x3; - let x3 = t1 + t2; - let t4 = t4 - x3; - let x3 = self.x + self.z; - let y3 = rhs.x + rhs.z; - let x3 = x3 * y3; - let y3 = t0 + t2; - let y3 = x3 - y3; - let x3 = t0 + t0; - let t0 = x3 + t0; - let t2 = mul_by_3b(t2); - let z3 = t1 + t2; - let t1 = t1 - t2; - let y3 = mul_by_3b(y3); - let x3 = t4 * y3; - let t2 = t3 * t1; - let x3 = t2 - x3; - let y3 = y3 * t0; - let t1 = t1 * z3; - let y3 = t1 + y3; - let t0 = t0 * t3; - let z3 = z3 * t4; - let z3 = z3 + t0; - - G2Projective { - x: x3, - y: y3, - z: z3, - } - } - - /// Adds this point to another point in the affine model. - pub fn add_mixed(&self, rhs: &G2Affine) -> G2Projective { - // Algorithm 8, https://eprint.iacr.org/2015/1060.pdf - - let t0 = self.x * rhs.x; - let t1 = self.y * rhs.y; - let t3 = rhs.x + rhs.y; - let t4 = self.x + self.y; - let t3 = t3 * t4; - let t4 = t0 + t1; - let t3 = t3 - t4; - let t4 = rhs.y * self.z; - let t4 = t4 + self.y; - let y3 = rhs.x * self.z; - let y3 = y3 + self.x; - let x3 = t0 + t0; - let t0 = x3 + t0; - let t2 = mul_by_3b(self.z); - let z3 = t1 + t2; - let t1 = t1 - t2; - let y3 = mul_by_3b(y3); - let x3 = t4 * y3; - let t2 = t3 * t1; - let x3 = t2 - x3; - let y3 = y3 * t0; - let t1 = t1 * z3; - let y3 = t1 + y3; - let t0 = t0 * t3; - let z3 = z3 * t4; - let z3 = z3 + t0; - - let tmp = G2Projective { - x: x3, - y: y3, - z: z3, - }; - - G2Projective::conditional_select(&tmp, self, rhs.is_identity()) - } - - fn multiply(&self, by: &[u8]) -> G2Projective { - let mut acc = G2Projective::identity(); - - // This is a simple double-and-add implementation of point - // multiplication, moving from most significant to least - // significant bit of the scalar. - // - // We skip the leading bit because it's always unset for Fq - // elements. - for bit in by - .iter() - .rev() - .flat_map(|byte| (0..8).rev().map(move |i| Choice::from((byte >> i) & 1u8))) - .skip(1) - { - acc = acc.double(); - acc = G2Projective::conditional_select(&acc, &(acc + self), bit); - } - - acc - } - - fn psi(&self) -> G2Projective { - // 1 / ((u+1) ^ ((q-1)/3)) - let psi_coeff_x = Fp2 { - c0: Fp::zero(), - c1: Fp::from_raw_unchecked([ - 0x890dc9e4867545c3, - 0x2af322533285a5d5, - 0x50880866309b7e2c, - 0xa20d1b8c7e881024, - 0x14e4f04fe2db9068, - 0x14e56d3f1564853a, - ]), - }; - // 1 / ((u+1) ^ (p-1)/2) - let psi_coeff_y = Fp2 { - c0: Fp::from_raw_unchecked([ - 0x3e2f585da55c9ad1, - 0x4294213d86c18183, - 0x382844c88b623732, - 0x92ad2afd19103e18, - 0x1d794e4fac7cf0b9, - 0x0bd592fc7d825ec8, - ]), - c1: Fp::from_raw_unchecked([ - 0x7bcfa7a25aa30fda, - 0xdc17dec12a927e7c, - 0x2f088dd86b4ebef1, - 0xd1ca2087da74d4a7, - 0x2da2596696cebc1d, - 0x0e2b7eedbbfd87d2, - ]), - }; - - G2Projective { - // x = frobenius(x)/((u+1)^((p-1)/3)) - x: self.x.frobenius_map() * psi_coeff_x, - // y = frobenius(y)/(u+1)^((p-1)/2) - y: self.y.frobenius_map() * psi_coeff_y, - // z = frobenius(z) - z: self.z.frobenius_map(), - } - } - - fn psi2(&self) -> G2Projective { - // 1 / 2 ^ ((q-1)/3) - let psi2_coeff_x = Fp2 { - c0: Fp::from_raw_unchecked([ - 0xcd03c9e48671f071, - 0x5dab22461fcda5d2, - 0x587042afd3851b95, - 0x8eb60ebe01bacb9e, - 0x03f97d6e83d050d2, - 0x18f0206554638741, - ]), - c1: Fp::zero(), - }; - - G2Projective { - // x = frobenius^2(x)/2^((p-1)/3); note that q^2 is the order of the field. - x: self.x * psi2_coeff_x, - // y = -frobenius^2(y); note that q^2 is the order of the field. - y: self.y.neg(), - // z = z - z: self.z, - } - } - - /// Multiply `self` by `crate::BLS_X`, using double and add. - fn mul_by_x(&self) -> G2Projective { - let mut xself = G2Projective::identity(); - // NOTE: in BLS12-381 we can just skip the first bit. - let mut x = crate::BLS_X >> 1; - let mut acc = *self; - while x != 0 { - acc = acc.double(); - if x % 2 == 1 { - xself += acc; - } - x >>= 1; - } - // finally, flip the sign - if crate::BLS_X_IS_NEGATIVE { - xself = -xself; - } - xself - } - - /// Clears the cofactor, using [Budroni-Pintore](https://ia.cr/2017/419). - /// This is equivalent to multiplying by $h\_\textrm{eff} = 3(z^2 - 1) \cdot - /// h_2$, where $h_2$ is the cofactor of $\mathbb{G}\_2$ and $z$ is the - /// parameter of BLS12-381. - pub fn clear_cofactor(&self) -> G2Projective { - let t1 = self.mul_by_x(); // [x] P - let t2 = self.psi(); // psi(P) - - self.double().psi2() // psi^2(2P) - + (t1 + t2).mul_by_x() // psi^2(2P) + [x^2] P + [x] psi(P) - - t1 // psi^2(2P) + [x^2 - x] P + [x] psi(P) - - t2 // psi^2(2P) + [x^2 - x] P + [x - 1] psi(P) - - self // psi^2(2P) + [x^2 - x - 1] P + [x - 1] psi(P) - } - - /// Converts a batch of `G2Projective` elements into `G2Affine` elements. This - /// function will panic if `p.len() != q.len()`. - pub fn batch_normalize(p: &[Self], q: &mut [G2Affine]) { - assert_eq!(p.len(), q.len()); - - let mut acc = Fp2::one(); - for (p, q) in p.iter().zip(q.iter_mut()) { - // We use the `x` field of `G2Affine` to store the product - // of previous z-coordinates seen. - q.x = acc; - - // We will end up skipping all identities in p - acc = Fp2::conditional_select(&(acc * p.z), &acc, p.is_identity()); - } - - // This is the inverse, as all z-coordinates are nonzero and the ones - // that are not are skipped. - acc = acc.invert().unwrap(); - - for (p, q) in p.iter().rev().zip(q.iter_mut().rev()) { - let skip = p.is_identity(); - - // Compute tmp = 1/z - let tmp = q.x * acc; - - // Cancel out z-coordinate in denominator of `acc` - acc = Fp2::conditional_select(&(acc * p.z), &acc, skip); - - // Set the coordinates to the correct value - q.x = p.x * tmp; - q.y = p.y * tmp; - q.infinity = Choice::from(0u8); - - *q = G2Affine::conditional_select(q, &G2Affine::identity(), skip); - } - } - - /// Returns true if this element is the identity (the point at infinity). - #[inline] - pub fn is_identity(&self) -> Choice { - self.z.is_zero() - } - - /// Returns true if this point is on the curve. This should always return - /// true unless an "unchecked" API was used. - pub fn is_on_curve(&self) -> Choice { - // Y^2 Z = X^3 + b Z^3 - - (self.y.square() * self.z).ct_eq(&(self.x.square() * self.x + self.z.square() * self.z * B)) - | self.z.is_zero() - } -} - -#[derive(Clone, Copy)] -pub struct G2Compressed([u8; 96]); - -impl fmt::Debug for G2Compressed { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.0[..].fmt(f) - } -} - -impl Default for G2Compressed { - fn default() -> Self { - G2Compressed([0; 96]) - } -} - -#[cfg(feature = "zeroize")] -impl zeroize::DefaultIsZeroes for G2Compressed {} - -impl AsRef<[u8]> for G2Compressed { - fn as_ref(&self) -> &[u8] { - &self.0 - } -} - -impl AsMut<[u8]> for G2Compressed { - fn as_mut(&mut self) -> &mut [u8] { - &mut self.0 - } -} - -impl ConstantTimeEq for G2Compressed { - fn ct_eq(&self, other: &Self) -> Choice { - self.0.ct_eq(&other.0) - } -} - -impl Eq for G2Compressed {} -impl PartialEq for G2Compressed { - #[inline] - fn eq(&self, other: &Self) -> bool { - bool::from(self.ct_eq(other)) - } -} - -#[derive(Clone, Copy)] -pub struct G2Uncompressed([u8; 192]); - -impl fmt::Debug for G2Uncompressed { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.0[..].fmt(f) - } -} - -impl Default for G2Uncompressed { - fn default() -> Self { - G2Uncompressed([0; 192]) - } -} - -#[cfg(feature = "zeroize")] -impl zeroize::DefaultIsZeroes for G2Uncompressed {} - -impl AsRef<[u8]> for G2Uncompressed { - fn as_ref(&self) -> &[u8] { - &self.0 - } -} - -impl AsMut<[u8]> for G2Uncompressed { - fn as_mut(&mut self) -> &mut [u8] { - &mut self.0 - } -} - -impl ConstantTimeEq for G2Uncompressed { - fn ct_eq(&self, other: &Self) -> Choice { - self.0.ct_eq(&other.0) - } -} - -impl Eq for G2Uncompressed {} -impl PartialEq for G2Uncompressed { - #[inline] - fn eq(&self, other: &Self) -> bool { - bool::from(self.ct_eq(other)) - } -} - -impl Group for G2Projective { - type Scalar = Scalar; - - fn random(mut rng: impl RngCore) -> Self { - loop { - let x = Fp2::random(&mut rng); - let flip_sign = rng.next_u32() % 2 != 0; - - // Obtain the corresponding y-coordinate given x as y = sqrt(x^3 + 4) - let p = ((x.square() * x) + B).sqrt().map(|y| G2Affine { - x, - y: if flip_sign { -y } else { y }, - infinity: 0.into(), - }); - - if p.is_some().into() { - let p = p.unwrap().to_curve().clear_cofactor(); - - if bool::from(!p.is_identity()) { - return p; - } - } - } - } - - fn identity() -> Self { - Self::identity() - } - - fn generator() -> Self { - Self::generator() - } - - fn is_identity(&self) -> Choice { - self.is_identity() - } - - #[must_use] - fn double(&self) -> Self { - self.double() - } -} - -#[cfg(feature = "alloc")] -impl WnafGroup for G2Projective { - fn recommended_wnaf_for_num_scalars(num_scalars: usize) -> usize { - const RECOMMENDATIONS: [usize; 11] = [1, 3, 8, 20, 47, 126, 260, 826, 1501, 4555, 84071]; - - let mut ret = 4; - for r in &RECOMMENDATIONS { - if num_scalars > *r { - ret += 1; - } else { - break; - } - } - - ret - } -} - -impl PrimeGroup for G2Projective {} - -impl Curve for G2Projective { - type AffineRepr = G2Affine; - - fn batch_normalize(p: &[Self], q: &mut [Self::AffineRepr]) { - Self::batch_normalize(p, q); - } - - fn to_affine(&self) -> Self::AffineRepr { - self.into() - } -} - -impl PrimeCurve for G2Projective { - type Affine = G2Affine; -} - -impl PrimeCurveAffine for G2Affine { - type Scalar = Scalar; - type Curve = G2Projective; - - fn identity() -> Self { - Self::identity() - } - - fn generator() -> Self { - Self::generator() - } - - fn is_identity(&self) -> Choice { - self.is_identity() - } - - fn to_curve(&self) -> Self::Curve { - self.into() - } -} - -impl GroupEncoding for G2Projective { - type Repr = G2Compressed; - - fn from_bytes(bytes: &Self::Repr) -> CtOption { - G2Affine::from_bytes(bytes).map(Self::from) - } - - fn from_bytes_unchecked(bytes: &Self::Repr) -> CtOption { - G2Affine::from_bytes_unchecked(bytes).map(Self::from) - } - - fn to_bytes(&self) -> Self::Repr { - G2Affine::from(self).to_bytes() - } -} - -impl GroupEncoding for G2Affine { - type Repr = G2Compressed; - - fn from_bytes(bytes: &Self::Repr) -> CtOption { - Self::from_compressed(&bytes.0) - } - - fn from_bytes_unchecked(bytes: &Self::Repr) -> CtOption { - Self::from_compressed_unchecked(&bytes.0) - } - - fn to_bytes(&self) -> Self::Repr { - G2Compressed(self.to_compressed()) - } -} - -impl UncompressedEncoding for G2Affine { - type Uncompressed = G2Uncompressed; - - fn from_uncompressed(bytes: &Self::Uncompressed) -> CtOption { - Self::from_uncompressed(&bytes.0) - } - - fn from_uncompressed_unchecked(bytes: &Self::Uncompressed) -> CtOption { - Self::from_uncompressed_unchecked(&bytes.0) - } - - fn to_uncompressed(&self) -> Self::Uncompressed { - G2Uncompressed(self.to_uncompressed()) - } -} - -#[test] -fn test_is_on_curve() { - assert!(bool::from(G2Affine::identity().is_on_curve())); - assert!(bool::from(G2Affine::generator().is_on_curve())); - assert!(bool::from(G2Projective::identity().is_on_curve())); - assert!(bool::from(G2Projective::generator().is_on_curve())); - - let z = Fp2 { - c0: Fp::from_raw_unchecked([ - 0xba7a_fa1f_9a6f_e250, - 0xfa0f_5b59_5eaf_e731, - 0x3bdc_4776_94c3_06e7, - 0x2149_be4b_3949_fa24, - 0x64aa_6e06_49b2_078c, - 0x12b1_08ac_3364_3c3e, - ]), - c1: Fp::from_raw_unchecked([ - 0x1253_25df_3d35_b5a8, - 0xdc46_9ef5_555d_7fe3, - 0x02d7_16d2_4431_06a9, - 0x05a1_db59_a6ff_37d0, - 0x7cf7_784e_5300_bb8f, - 0x16a8_8922_c7a5_e844, - ]), - }; - - let gen = G2Affine::generator(); - let mut test = G2Projective { - x: gen.x * z, - y: gen.y * z, - z, - }; - - assert!(bool::from(test.is_on_curve())); - - test.x = z; - assert!(!bool::from(test.is_on_curve())); -} - -#[test] -#[allow(clippy::eq_op)] -fn test_affine_point_equality() { - let a = G2Affine::generator(); - let b = G2Affine::identity(); - - assert!(a == a); - assert!(b == b); - assert!(a != b); - assert!(b != a); -} - -#[test] -#[allow(clippy::eq_op)] -fn test_projective_point_equality() { - let a = G2Projective::generator(); - let b = G2Projective::identity(); - - assert!(a == a); - assert!(b == b); - assert!(a != b); - assert!(b != a); - - let z = Fp2 { - c0: Fp::from_raw_unchecked([ - 0xba7a_fa1f_9a6f_e250, - 0xfa0f_5b59_5eaf_e731, - 0x3bdc_4776_94c3_06e7, - 0x2149_be4b_3949_fa24, - 0x64aa_6e06_49b2_078c, - 0x12b1_08ac_3364_3c3e, - ]), - c1: Fp::from_raw_unchecked([ - 0x1253_25df_3d35_b5a8, - 0xdc46_9ef5_555d_7fe3, - 0x02d7_16d2_4431_06a9, - 0x05a1_db59_a6ff_37d0, - 0x7cf7_784e_5300_bb8f, - 0x16a8_8922_c7a5_e844, - ]), - }; - - let mut c = G2Projective { - x: a.x * z, - y: a.y * z, - z, - }; - assert!(bool::from(c.is_on_curve())); - - assert!(a == c); - assert!(b != c); - assert!(c == a); - assert!(c != b); - - c.y = -c.y; - assert!(bool::from(c.is_on_curve())); - - assert!(a != c); - assert!(b != c); - assert!(c != a); - assert!(c != b); - - c.y = -c.y; - c.x = z; - assert!(!bool::from(c.is_on_curve())); - assert!(a != b); - assert!(a != c); - assert!(b != c); -} - -#[test] -fn test_conditionally_select_affine() { - let a = G2Affine::generator(); - let b = G2Affine::identity(); - - assert_eq!(G2Affine::conditional_select(&a, &b, Choice::from(0u8)), a); - assert_eq!(G2Affine::conditional_select(&a, &b, Choice::from(1u8)), b); -} - -#[test] -fn test_conditionally_select_projective() { - let a = G2Projective::generator(); - let b = G2Projective::identity(); - - assert_eq!( - G2Projective::conditional_select(&a, &b, Choice::from(0u8)), - a - ); - assert_eq!( - G2Projective::conditional_select(&a, &b, Choice::from(1u8)), - b - ); -} - -#[test] -fn test_projective_to_affine() { - let a = G2Projective::generator(); - let b = G2Projective::identity(); - - assert!(bool::from(G2Affine::from(a).is_on_curve())); - assert!(!bool::from(G2Affine::from(a).is_identity())); - assert!(bool::from(G2Affine::from(b).is_on_curve())); - assert!(bool::from(G2Affine::from(b).is_identity())); - - let z = Fp2 { - c0: Fp::from_raw_unchecked([ - 0xba7a_fa1f_9a6f_e250, - 0xfa0f_5b59_5eaf_e731, - 0x3bdc_4776_94c3_06e7, - 0x2149_be4b_3949_fa24, - 0x64aa_6e06_49b2_078c, - 0x12b1_08ac_3364_3c3e, - ]), - c1: Fp::from_raw_unchecked([ - 0x1253_25df_3d35_b5a8, - 0xdc46_9ef5_555d_7fe3, - 0x02d7_16d2_4431_06a9, - 0x05a1_db59_a6ff_37d0, - 0x7cf7_784e_5300_bb8f, - 0x16a8_8922_c7a5_e844, - ]), - }; - - let c = G2Projective { - x: a.x * z, - y: a.y * z, - z, - }; - - assert_eq!(G2Affine::from(c), G2Affine::generator()); -} - -#[test] -fn test_affine_to_projective() { - let a = G2Affine::generator(); - let b = G2Affine::identity(); - - assert!(bool::from(G2Projective::from(a).is_on_curve())); - assert!(!bool::from(G2Projective::from(a).is_identity())); - assert!(bool::from(G2Projective::from(b).is_on_curve())); - assert!(bool::from(G2Projective::from(b).is_identity())); -} - -#[test] -fn test_doubling() { - { - let tmp = G2Projective::identity().double(); - assert!(bool::from(tmp.is_identity())); - assert!(bool::from(tmp.is_on_curve())); - } - { - let tmp = G2Projective::generator().double(); - assert!(!bool::from(tmp.is_identity())); - assert!(bool::from(tmp.is_on_curve())); - - assert_eq!( - G2Affine::from(tmp), - G2Affine { - x: Fp2 { - c0: Fp::from_raw_unchecked([ - 0xe9d9_e2da_9620_f98b, - 0x54f1_1993_46b9_7f36, - 0x3db3_b820_376b_ed27, - 0xcfdb_31c9_b0b6_4f4c, - 0x41d7_c127_8635_4493, - 0x0571_0794_c255_c064, - ]), - c1: Fp::from_raw_unchecked([ - 0xd6c1_d3ca_6ea0_d06e, - 0xda0c_bd90_5595_489f, - 0x4f53_52d4_3479_221d, - 0x8ade_5d73_6f8c_97e0, - 0x48cc_8433_925e_f70e, - 0x08d7_ea71_ea91_ef81, - ]), - }, - y: Fp2 { - c0: Fp::from_raw_unchecked([ - 0x15ba_26eb_4b0d_186f, - 0x0d08_6d64_b7e9_e01e, - 0xc8b8_48dd_652f_4c78, - 0xeecf_46a6_123b_ae4f, - 0x255e_8dd8_b6dc_812a, - 0x1641_42af_21dc_f93f, - ]), - c1: Fp::from_raw_unchecked([ - 0xf9b4_a1a8_9598_4db4, - 0xd417_b114_cccf_f748, - 0x6856_301f_c89f_086e, - 0x41c7_7787_8931_e3da, - 0x3556_b155_066a_2105, - 0x00ac_f7d3_25cb_89cf, - ]), - }, - infinity: Choice::from(0u8) - } - ); - } -} - -#[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([ - 0xba7a_fa1f_9a6f_e250, - 0xfa0f_5b59_5eaf_e731, - 0x3bdc_4776_94c3_06e7, - 0x2149_be4b_3949_fa24, - 0x64aa_6e06_49b2_078c, - 0x12b1_08ac_3364_3c3e, - ]), - c1: Fp::from_raw_unchecked([ - 0x1253_25df_3d35_b5a8, - 0xdc46_9ef5_555d_7fe3, - 0x02d7_16d2_4431_06a9, - 0x05a1_db59_a6ff_37d0, - 0x7cf7_784e_5300_bb8f, - 0x16a8_8922_c7a5_e844, - ]), - }; - - b = G2Projective { - x: b.x * z, - y: b.y * 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([ - 0xba7a_fa1f_9a6f_e250, - 0xfa0f_5b59_5eaf_e731, - 0x3bdc_4776_94c3_06e7, - 0x2149_be4b_3949_fa24, - 0x64aa_6e06_49b2_078c, - 0x12b1_08ac_3364_3c3e, - ]), - c1: Fp::from_raw_unchecked([ - 0x1253_25df_3d35_b5a8, - 0xdc46_9ef5_555d_7fe3, - 0x02d7_16d2_4431_06a9, - 0x05a1_db59_a6ff_37d0, - 0x7cf7_784e_5300_bb8f, - 0x16a8_8922_c7a5_e844, - ]), - }; - - b = G2Projective { - x: b.x * z, - y: b.y * 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 += 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([ - 0xcd03_c9e4_8671_f071, - 0x5dab_2246_1fcd_a5d2, - 0x5870_42af_d385_1b95, - 0x8eb6_0ebe_01ba_cb9e, - 0x03f9_7d6e_83d0_50d2, - 0x18f0_2065_5463_8741, - ]), - 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([ - 0x705a_bc79_9ca7_73d3, - 0xfe13_2292_c1d4_bf08, - 0xf37e_ce3e_07b2_b466, - 0x887e_1c43_f447_e301, - 0x1e09_70d0_33bc_77e8, - 0x1985_c81e_20a6_93f2, - ]), - c1: Fp::from_raw_unchecked([ - 0x1d79_b25d_b36a_b924, - 0x2394_8e4d_5296_39d3, - 0x471b_a7fb_0d00_6297, - 0x2c36_d4b4_465d_c4c0, - 0x82bb_c3cf_ec67_f538, - 0x051d_2728_b67b_f952, - ]) - }, - y: Fp2 { - c0: Fp::from_raw_unchecked([ - 0x41b1_bbf6_576c_0abf, - 0xb6cc_9371_3f7a_0f9a, - 0x6b65_b43e_48f3_f01f, - 0xfb7a_4cfc_af81_be4f, - 0x3e32_dadc_6ec2_2cb6, - 0x0bb0_fc49_d798_07e3, - ]), - c1: Fp::from_raw_unchecked([ - 0x7d13_9778_8f5f_2ddf, - 0xab29_0714_4ff0_d8e8, - 0x5b75_73e0_cdb9_1f92, - 0x4cb8_932d_d31d_af28, - 0x62bb_fac6_db05_2a54, - 0x11f9_5c16_d14c_3bbe, - ]) - }, - z: Fp2::one() - }) - ); - assert!(!bool::from(c.is_identity())); - assert!(bool::from(c.is_on_curve())); - } -} - -#[test] -fn test_mixed_addition() { - { - let a = G2Affine::identity(); - let b = G2Projective::identity(); - let c = a + b; - assert!(bool::from(c.is_identity())); - assert!(bool::from(c.is_on_curve())); - } - { - let a = G2Affine::identity(); - let mut b = G2Projective::generator(); - { - let z = Fp2 { - c0: Fp::from_raw_unchecked([ - 0xba7a_fa1f_9a6f_e250, - 0xfa0f_5b59_5eaf_e731, - 0x3bdc_4776_94c3_06e7, - 0x2149_be4b_3949_fa24, - 0x64aa_6e06_49b2_078c, - 0x12b1_08ac_3364_3c3e, - ]), - c1: Fp::from_raw_unchecked([ - 0x1253_25df_3d35_b5a8, - 0xdc46_9ef5_555d_7fe3, - 0x02d7_16d2_4431_06a9, - 0x05a1_db59_a6ff_37d0, - 0x7cf7_784e_5300_bb8f, - 0x16a8_8922_c7a5_e844, - ]), - }; - - b = G2Projective { - x: b.x * z, - y: b.y * 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 = G2Affine::identity(); - let mut b = G2Projective::generator(); - { - let z = Fp2 { - c0: Fp::from_raw_unchecked([ - 0xba7a_fa1f_9a6f_e250, - 0xfa0f_5b59_5eaf_e731, - 0x3bdc_4776_94c3_06e7, - 0x2149_be4b_3949_fa24, - 0x64aa_6e06_49b2_078c, - 0x12b1_08ac_3364_3c3e, - ]), - c1: Fp::from_raw_unchecked([ - 0x1253_25df_3d35_b5a8, - 0xdc46_9ef5_555d_7fe3, - 0x02d7_16d2_4431_06a9, - 0x05a1_db59_a6ff_37d0, - 0x7cf7_784e_5300_bb8f, - 0x16a8_8922_c7a5_e844, - ]), - }; - - b = G2Projective { - x: b.x * z, - y: b.y * 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 += G2Affine::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([ - 0xcd03_c9e4_8671_f071, - 0x5dab_2246_1fcd_a5d2, - 0x5870_42af_d385_1b95, - 0x8eb6_0ebe_01ba_cb9e, - 0x03f9_7d6e_83d0_50d2, - 0x18f0_2065_5463_8741, - ]), - 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, - }; - let a = G2Affine::from(a); - 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([ - 0x705a_bc79_9ca7_73d3, - 0xfe13_2292_c1d4_bf08, - 0xf37e_ce3e_07b2_b466, - 0x887e_1c43_f447_e301, - 0x1e09_70d0_33bc_77e8, - 0x1985_c81e_20a6_93f2, - ]), - c1: Fp::from_raw_unchecked([ - 0x1d79_b25d_b36a_b924, - 0x2394_8e4d_5296_39d3, - 0x471b_a7fb_0d00_6297, - 0x2c36_d4b4_465d_c4c0, - 0x82bb_c3cf_ec67_f538, - 0x051d_2728_b67b_f952, - ]) - }, - y: Fp2 { - c0: Fp::from_raw_unchecked([ - 0x41b1_bbf6_576c_0abf, - 0xb6cc_9371_3f7a_0f9a, - 0x6b65_b43e_48f3_f01f, - 0xfb7a_4cfc_af81_be4f, - 0x3e32_dadc_6ec2_2cb6, - 0x0bb0_fc49_d798_07e3, - ]), - c1: Fp::from_raw_unchecked([ - 0x7d13_9778_8f5f_2ddf, - 0xab29_0714_4ff0_d8e8, - 0x5b75_73e0_cdb9_1f92, - 0x4cb8_932d_d31d_af28, - 0x62bb_fac6_db05_2a54, - 0x11f9_5c16_d14c_3bbe, - ]) - }, - z: Fp2::one() - }) - ); - assert!(!bool::from(c.is_identity())); - assert!(bool::from(c.is_on_curve())); - } -} - -#[test] -#[allow(clippy::eq_op)] -fn test_projective_negation_and_subtraction() { - let a = G2Projective::generator().double(); - assert_eq!(a + (-a), G2Projective::identity()); - assert_eq!(a + (-a), a - a); -} - -#[test] -fn test_affine_negation_and_subtraction() { - let a = G2Affine::generator(); - assert_eq!(G2Projective::from(a) + (-a), G2Projective::identity()); - assert_eq!(G2Projective::from(a) + (-a), G2Projective::from(a) - a); -} - -#[test] -fn test_projective_scalar_multiplication() { - let g = G2Projective::generator(); - let a = Scalar::from_raw([ - 0x2b56_8297_a56d_a71c, - 0xd8c3_9ecb_0ef3_75d1, - 0x435c_38da_67bf_bf96, - 0x8088_a050_26b6_59b2, - ]); - let b = Scalar::from_raw([ - 0x785f_dd9b_26ef_8b85, - 0xc997_f258_3769_5c18, - 0x4c8d_bc39_e7b7_56c1, - 0x70d9_b6cc_6d87_df20, - ]); - let c = a * b; - - assert_eq!((g * a) * b, g * c); -} - -#[test] -fn test_affine_scalar_multiplication() { - let g = G2Affine::generator(); - let a = Scalar::from_raw([ - 0x2b56_8297_a56d_a71c, - 0xd8c3_9ecb_0ef3_75d1, - 0x435c_38da_67bf_bf96, - 0x8088_a050_26b6_59b2, - ]); - let b = Scalar::from_raw([ - 0x785f_dd9b_26ef_8b85, - 0xc997_f258_3769_5c18, - 0x4c8d_bc39_e7b7_56c1, - 0x70d9_b6cc_6d87_df20, - ]); - let c = a * b; - - assert_eq!(G2Affine::from(g * a) * b, g * c); -} - -#[test] -fn test_is_torsion_free() { - let a = G2Affine { - x: Fp2 { - c0: Fp::from_raw_unchecked([ - 0x89f5_50c8_13db_6431, - 0xa50b_e8c4_56cd_8a1a, - 0xa45b_3741_14ca_e851, - 0xbb61_90f5_bf7f_ff63, - 0x970c_a02c_3ba8_0bc7, - 0x02b8_5d24_e840_fbac, - ]), - c1: Fp::from_raw_unchecked([ - 0x6888_bc53_d707_16dc, - 0x3dea_6b41_1768_2d70, - 0xd8f5_f930_500c_a354, - 0x6b5e_cb65_56f5_c155, - 0xc96b_ef04_3477_8ab0, - 0x0508_1505_5150_06ad, - ]), - }, - y: Fp2 { - c0: Fp::from_raw_unchecked([ - 0x3cf1_ea0d_434b_0f40, - 0x1a0d_c610_e603_e333, - 0x7f89_9561_60c7_2fa0, - 0x25ee_03de_cf64_31c5, - 0xeee8_e206_ec0f_e137, - 0x0975_92b2_26df_ef28, - ]), - c1: Fp::from_raw_unchecked([ - 0x71e8_bb5f_2924_7367, - 0xa5fe_049e_2118_31ce, - 0x0ce6_b354_502a_3896, - 0x93b0_1200_0997_314e, - 0x6759_f3b6_aa5b_42ac, - 0x1569_44c4_dfe9_2bbb, - ]), - }, - infinity: Choice::from(0u8), - }; - assert!(!bool::from(a.is_torsion_free())); - - assert!(bool::from(G2Affine::identity().is_torsion_free())); - assert!(bool::from(G2Affine::generator().is_torsion_free())); -} - -#[test] -fn test_mul_by_x() { - // multiplying by `x` a point in G2 is the same as multiplying by - // the equivalent scalar. - let generator = G2Projective::generator(); - let x = if crate::BLS_X_IS_NEGATIVE { - -Scalar::from(crate::BLS_X) - } else { - Scalar::from(crate::BLS_X) - }; - assert_eq!(generator.mul_by_x(), generator * x); - - let point = G2Projective::generator() * Scalar::from(42); - assert_eq!(point.mul_by_x(), point * x); -} - -#[test] -fn test_psi() { - let generator = G2Projective::generator(); - - let z = Fp2 { - c0: Fp::from_raw_unchecked([ - 0x0ef2ddffab187c0a, - 0x2424522b7d5ecbfc, - 0xc6f341a3398054f4, - 0x5523ddf409502df0, - 0xd55c0b5a88e0dd97, - 0x066428d704923e52, - ]), - c1: Fp::from_raw_unchecked([ - 0x538bbe0c95b4878d, - 0xad04a50379522881, - 0x6d5c05bf5c12fb64, - 0x4ce4a069a2d34787, - 0x59ea6c8d0dffaeaf, - 0x0d42a083a75bd6f3, - ]), - }; - - // `point` is a random point in the curve - let point = G2Projective { - x: Fp2 { - c0: Fp::from_raw_unchecked([ - 0xee4c8cb7c047eaf2, - 0x44ca22eee036b604, - 0x33b3affb2aefe101, - 0x15d3e45bbafaeb02, - 0x7bfc2154cd7419a4, - 0x0a2d0c2b756e5edc, - ]), - c1: Fp::from_raw_unchecked([ - 0xfc224361029a8777, - 0x4cbf2baab8740924, - 0xc5008c6ec6592c89, - 0xecc2c57b472a9c2d, - 0x8613eafd9d81ffb1, - 0x10fe54daa2d3d495, - ]), - } * z, - y: Fp2 { - c0: Fp::from_raw_unchecked([ - 0x7de7edc43953b75c, - 0x58be1d2de35e87dc, - 0x5731d30b0e337b40, - 0xbe93b60cfeaae4c9, - 0x8b22c203764bedca, - 0x01616c8d1033b771, - ]), - c1: Fp::from_raw_unchecked([ - 0xea126fe476b5733b, - 0x85cee68b5dae1652, - 0x98247779f7272b04, - 0xa649c8b468c6e808, - 0xb5b9a62dff0c4e45, - 0x1555b67fc7bbe73d, - ]), - }, - z: z.square() * z, - }; - assert!(bool::from(point.is_on_curve())); - - // psi2(P) = psi(psi(P)) - assert_eq!(generator.psi2(), generator.psi().psi()); - assert_eq!(point.psi2(), point.psi().psi()); - // psi(P) is a morphism - assert_eq!(generator.double().psi(), generator.psi().double()); - assert_eq!(point.psi() + generator.psi(), (point + generator).psi()); - // psi(P) behaves in the same way on the same projective point - let mut normalized_point = [G2Affine::identity()]; - G2Projective::batch_normalize(&[point], &mut normalized_point); - let normalized_point = G2Projective::from(normalized_point[0]); - assert_eq!(point.psi(), normalized_point.psi()); - assert_eq!(point.psi2(), normalized_point.psi2()); -} - -#[test] -fn test_clear_cofactor() { - let z = Fp2 { - c0: Fp::from_raw_unchecked([ - 0x0ef2ddffab187c0a, - 0x2424522b7d5ecbfc, - 0xc6f341a3398054f4, - 0x5523ddf409502df0, - 0xd55c0b5a88e0dd97, - 0x066428d704923e52, - ]), - c1: Fp::from_raw_unchecked([ - 0x538bbe0c95b4878d, - 0xad04a50379522881, - 0x6d5c05bf5c12fb64, - 0x4ce4a069a2d34787, - 0x59ea6c8d0dffaeaf, - 0x0d42a083a75bd6f3, - ]), - }; - - // `point` is a random point in the curve - let point = G2Projective { - x: Fp2 { - c0: Fp::from_raw_unchecked([ - 0xee4c8cb7c047eaf2, - 0x44ca22eee036b604, - 0x33b3affb2aefe101, - 0x15d3e45bbafaeb02, - 0x7bfc2154cd7419a4, - 0x0a2d0c2b756e5edc, - ]), - c1: Fp::from_raw_unchecked([ - 0xfc224361029a8777, - 0x4cbf2baab8740924, - 0xc5008c6ec6592c89, - 0xecc2c57b472a9c2d, - 0x8613eafd9d81ffb1, - 0x10fe54daa2d3d495, - ]), - } * z, - y: Fp2 { - c0: Fp::from_raw_unchecked([ - 0x7de7edc43953b75c, - 0x58be1d2de35e87dc, - 0x5731d30b0e337b40, - 0xbe93b60cfeaae4c9, - 0x8b22c203764bedca, - 0x01616c8d1033b771, - ]), - c1: Fp::from_raw_unchecked([ - 0xea126fe476b5733b, - 0x85cee68b5dae1652, - 0x98247779f7272b04, - 0xa649c8b468c6e808, - 0xb5b9a62dff0c4e45, - 0x1555b67fc7bbe73d, - ]), - }, - z: z.square() * z, - }; - - assert!(bool::from(point.is_on_curve())); - assert!(!bool::from(G2Affine::from(point).is_torsion_free())); - let cleared_point = point.clear_cofactor(); - - assert!(bool::from(cleared_point.is_on_curve())); - assert!(bool::from(G2Affine::from(cleared_point).is_torsion_free())); - - // the generator (and the identity) are always on the curve, - // even after clearing the cofactor - let generator = G2Projective::generator(); - assert!(bool::from(generator.clear_cofactor().is_on_curve())); - let id = G2Projective::identity(); - assert!(bool::from(id.clear_cofactor().is_on_curve())); - - // test the effect on q-torsion points multiplying by h_eff modulo |Scalar| - // h_eff % q = 0x2b116900400069009a40200040001ffff - let h_eff_modq: [u8; 32] = [ - 0xff, 0xff, 0x01, 0x00, 0x04, 0x00, 0x02, 0xa4, 0x09, 0x90, 0x06, 0x00, 0x04, 0x90, 0x16, - 0xb1, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, - ]; - assert_eq!(generator.clear_cofactor(), generator.multiply(&h_eff_modq)); - assert_eq!( - cleared_point.clear_cofactor(), - cleared_point.multiply(&h_eff_modq) - ); -} - -#[test] -fn test_batch_normalize() { - let a = G2Projective::generator().double(); - let b = a.double(); - let c = b.double(); - - for a_identity in (0..=1).map(|n| n == 1) { - for b_identity in (0..=1).map(|n| n == 1) { - for c_identity in (0..=1).map(|n| n == 1) { - let mut v = [a, b, c]; - if a_identity { - v[0] = G2Projective::identity() - } - if b_identity { - v[1] = G2Projective::identity() - } - if c_identity { - v[2] = G2Projective::identity() - } - - let mut t = [ - G2Affine::identity(), - G2Affine::identity(), - G2Affine::identity(), - ]; - let expected = [ - G2Affine::from(v[0]), - G2Affine::from(v[1]), - G2Affine::from(v[2]), - ]; - - G2Projective::batch_normalize(&v[..], &mut t[..]); - - assert_eq!(&t[..], &expected[..]); - } - } - } -} - -#[cfg(feature = "zeroize")] -#[test] -fn test_zeroize() { - use zeroize::Zeroize; - - let mut a = G2Affine::generator(); - a.zeroize(); - assert!(bool::from(a.is_identity())); - - let mut a = G2Projective::generator(); - a.zeroize(); - assert!(bool::from(a.is_identity())); - - let mut a = GroupEncoding::to_bytes(&G2Affine::generator()); - a.zeroize(); - assert_eq!(&a, &G2Compressed::default()); - - let mut a = UncompressedEncoding::to_uncompressed(&G2Affine::generator()); - a.zeroize(); - assert_eq!(&a, &G2Uncompressed::default()); -} - -#[test] -fn test_commutative_scalar_subgroup_multiplication() { - let a = Scalar::from_raw([ - 0x1fff_3231_233f_fffd, - 0x4884_b7fa_0003_4802, - 0x998c_4fef_ecbc_4ff3, - 0x1824_b159_acc5_0562, - ]); - - let g2_a = G2Affine::generator(); - let g2_p = G2Projective::generator(); - - // By reference. - assert_eq!(&g2_a * &a, &a * &g2_a); - assert_eq!(&g2_p * &a, &a * &g2_p); - - // Mixed - assert_eq!(&g2_a * a.clone(), a.clone() * &g2_a); - assert_eq!(&g2_p * a.clone(), a.clone() * &g2_p); - assert_eq!(g2_a.clone() * &a, &a * g2_a.clone()); - assert_eq!(g2_p.clone() * &a, &a * g2_p.clone()); - - // By value. - assert_eq!(g2_p * a, a * g2_p); - assert_eq!(g2_a * a, a * g2_a); -} diff --git a/constantine/bls12_381/src/hash_to_curve/chain.rs b/constantine/bls12_381/src/hash_to_curve/chain.rs deleted file mode 100644 index c9d2352f2..000000000 --- a/constantine/bls12_381/src/hash_to_curve/chain.rs +++ /dev/null @@ -1,920 +0,0 @@ -//! Addition chains for computing square roots. -//! chain_pm3div4: input x, output x^((p-3)//4). -//! chain_p2m9div16: input x, output x^((p**2 - 9) // 16). - -use core::ops::MulAssign; - -use crate::{fp::Fp, fp2::Fp2}; - -macro_rules! square { - ($var:expr, $n:expr) => { - for _ in 0..$n { - $var = $var.square(); - } - }; -} - -#[allow(clippy::cognitive_complexity)] -/// addchain for 1000602388805416848354447456433976039139220704984751971333014534031007912622709466110671907282253916009473568139946 -/// Bos-Coster (win=4) : 458 links, 16 variables */ -/// Addition chain implementing exponentiation by (p - 3) // 4. -pub fn chain_pm3div4(var0: &Fp) -> Fp { - let mut var1 = var0.square(); - //Self::sqr(var1, var0); /* 0 : 2 */ - let var9 = var1 * var0; - //Self::mul(&mut var9, var1, var0); /* 1 : 3 */ - let var5 = var1.square(); - //Self::sqr(&mut var5, var1); /* 2 : 4 */ - let var2 = var9 * var1; - //Self::mul(&mut var2, &var9, var1); /* 3 : 5 */ - let var7 = var5 * var9; - //Self::mul(&mut var7, &var5, &var9); /* 4 : 7 */ - let var10 = var2 * var5; - //Self::mul(&mut var10, &var2, &var5); /* 5 : 9 */ - let var13 = var7 * var5; - //Self::mul(&mut var13, &var7, &var5); /* 6 : 11 */ - let var4 = var10 * var5; - //Self::mul(&mut var4, &var10, &var5); /* 7 : 13 */ - let var8 = var13 * var5; - //Self::mul(&mut var8, &var13, &var5); /* 8 : 15 */ - let var15 = var4 * var5; - //Self::mul(&mut var15, &var4, &var5); /* 9 : 17 */ - let var11 = var8 * var5; - //Self::mul(&mut var11, &var8, &var5); /* 10 : 19 */ - let var3 = var15 * var5; - //Self::mul(&mut var3, &var15, &var5); /* 11 : 21 */ - let var12 = var11 * var5; - //Self::mul(&mut var12, &var11, &var5); /* 12 : 23 */ - var1 = var4.square(); - //Self::sqr(var1, &var4); /* 13 : 26 */ - let var14 = var12 * var5; - //Self::mul(&mut var14, &var12, &var5); /* 14 : 27 */ - let var6 = var1 * var9; - //Self::mul(&mut var6, var1, &var9); /* 15 : 29 */ - let var5 = var1 * var2; - //Self::mul(&mut var5, var1, &var2); /* 16 : 31 */ - // 17 : 106496 - square!(var1, 12); - // 29 : 106513 - var1.mul_assign(&var15); - // 30 : 13633664 - square!(var1, 7); - // 37 : 13633679 - var1.mul_assign(&var8); - // 38 : 218138864 - square!(var1, 4); - // 42 : 218138869 - var1.mul_assign(&var2); - // 43 : 13960887616 - square!(var1, 6); - // 49 : 13960887623 - var1.mul_assign(&var7); - // 50 : 1786993615744 - square!(var1, 7); - // 57 : 1786993615767 - var1.mul_assign(&var12); - // 58 : 57183795704544 - square!(var1, 5); - // 63 : 57183795704575 - var1.mul_assign(&var5); - // 64 : 228735182818300 - square!(var1, 2); - // 66 : 228735182818303 - var1.mul_assign(&var9); - // 67 : 14639051700371392 - square!(var1, 6); - // 73 : 14639051700371405 - var1.mul_assign(&var4); - // 74 : 936899308823769920 - square!(var1, 6); - // 80 : 936899308823769933 - var1.mul_assign(&var4); - // 81 : 59961555764721275712 - square!(var1, 6); - // 87 : 59961555764721275721 - var1.mul_assign(&var10); - // 88 : 479692446117770205768 - square!(var1, 3); - // 91 : 479692446117770205771 - var1.mul_assign(&var9); - // 92 : 61400633103074586338688 - square!(var1, 7); - // 99 : 61400633103074586338701 - var1.mul_assign(&var4); - // 100 : 982410129649193381419216 - square!(var1, 4); - // 104 : 982410129649193381419229 - var1.mul_assign(&var4); - // 105 : 62874248297548376410830656 - square!(var1, 6); - // 111 : 62874248297548376410830671 - var1.mul_assign(&var8); - // 112 : 4023951891043096090293162944 - square!(var1, 6); - // 118 : 4023951891043096090293162971 - var1.mul_assign(&var14); - // 119 : 32191615128344768722345303768 - square!(var1, 3); - // 122 : 32191615128344768722345303769 - var1.mul_assign(var0); - // 123 : 8241053472856260792920397764864 - square!(var1, 8); - // 131 : 8241053472856260792920397764877 - var1.mul_assign(&var4); - // 132 : 1054854844525601381493810913904256 - square!(var1, 7); - // 139 : 1054854844525601381493810913904279 - var1.mul_assign(&var12); - // 140 : 33755355024819244207801949244936928 - square!(var1, 5); - // 145 : 33755355024819244207801949244936939 - var1.mul_assign(&var13); - // 146 : 2160342721588431629299324751675964096 - square!(var1, 6); - // 152 : 2160342721588431629299324751675964109 - var1.mul_assign(&var4); - // 153 : 138261934181659624275156784107261702976 - square!(var1, 6); - // 159 : 138261934181659624275156784107261703005 - var1.mul_assign(&var6); - // 160 : 2212190946906553988402508545716187248080 - square!(var1, 4); - // 164 : 2212190946906553988402508545716187248089 - var1.mul_assign(&var10); - // 165 : 566320882408077821031042187703343935510784 - square!(var1, 8); - // 173 : 566320882408077821031042187703343935510813 - var1.mul_assign(&var6); - // 174 : 9061134118529245136496675003253502968173008 - square!(var1, 4); - // 178 : 9061134118529245136496675003253502968173021 - var1.mul_assign(&var4); - // 179 : 1159825167171743377471574400416448379926146688 - square!(var1, 7); - // 186 : 1159825167171743377471574400416448379926146711 - var1.mul_assign(&var12); - // 187 : 593830485591932609265446093013221570522187116032 - square!(var1, 9); - // 196 : 593830485591932609265446093013221570522187116051 - var1.mul_assign(&var11); - // 197 : 2375321942367730437061784372052886282088748464204 - square!(var1, 2); - // 199 : 2375321942367730437061784372052886282088748464207 - var1.mul_assign(&var9); - // 200 : 76010302155767373985977099905692361026839950854624 - square!(var1, 5); - // 205 : 76010302155767373985977099905692361026839950854631 - var1.mul_assign(&var7); - // 206 : 9729318675938223870205068787928622211435513709392768 - square!(var1, 7); - // 213 : 9729318675938223870205068787928622211435513709392773 - var1.mul_assign(&var2); - // 214 : 1245352790520092655386248804854863643063745754802274944 - square!(var1, 7); - // 221 : 1245352790520092655386248804854863643063745754802274953 - var1.mul_assign(&var10); - // 222 : 79702578593285929944719923510711273156079728307345596992 - square!(var1, 6); - // 228 : 79702578593285929944719923510711273156079728307345597015 - var1.mul_assign(&var12); - // 229 : 2550482514985149758231037552342760740994551305835059104480 - square!(var1, 5); - // 234 : 2550482514985149758231037552342760740994551305835059104509 - var1.mul_assign(&var6); - // 235 : 81615440479524792263393201674968343711825641786721891344288 - square!(var1, 5); - // 240 : 81615440479524792263393201674968343711825641786721891344307 - var1.mul_assign(&var11); - // 241 : 2611694095344793352428582453598986998778420537175100523017824 - square!(var1, 5); - // 246 : 2611694095344793352428582453598986998778420537175100523017843 - var1.mul_assign(&var11); - // 247 : 668593688408267098221717108121340671687275657516825733892567808 - square!(var1, 8); - // 255 : 668593688408267098221717108121340671687275657516825733892567821 - var1.mul_assign(&var4); - // 256 : 85579992116258188572379789839531605975971284162153693938248681088 - square!(var1, 7); - // 263 : 85579992116258188572379789839531605975971284162153693938248681109 - var1.mul_assign(&var3); - // 264 : 43816955963524192549058452397840182259697297491022691296383324727808 - square!(var1, 9); - // 273 : 43816955963524192549058452397840182259697297491022691296383324727823 - var1.mul_assign(&var8); - // 274 : 1402142590832774161569870476730885832310313519712726121484266391290336 - square!(var1, 5); - // 279 : 1402142590832774161569870476730885832310313519712726121484266391290349 - var1.mul_assign(&var4); - // 280 : 11217140726662193292558963813847086658482508157701808971874131130322792 - square!(var1, 3); - // 283 : 11217140726662193292558963813847086658482508157701808971874131130322795 - var1.mul_assign(&var9); - // 284 : 2871588026025521482895094736344854184571522088371663096799777569362635520 - square!(var1, 8); - // 292 : 2871588026025521482895094736344854184571522088371663096799777569362635535 - var1.mul_assign(&var8); - // 293 : 22972704208204171863160757890758833476572176706973304774398220554901084280 - square!(var1, 3); - // 296 : 22972704208204171863160757890758833476572176706973304774398220554901084283 - var1.mul_assign(&var9); - // 297 : 2940506138650133998484577010017130685001238618492583011122972231027338788224 - square!(var1, 7); - // 304 : 2940506138650133998484577010017130685001238618492583011122972231027338788233 - var1.mul_assign(&var10); - // 305 : 1505539142988868607224103429128770910720634172668202501694961782285997459575296 - square!(var1, 9); - // 314 : 1505539142988868607224103429128770910720634172668202501694961782285997459575311 - var1.mul_assign(&var8); - // 315 : 96354505151287590862342619464241338286120587050764960108477554066303837412819904 - square!(var1, 6); - // 321 : 96354505151287590862342619464241338286120587050764960108477554066303837412819925 - var1.mul_assign(&var3); - // 322 : 6166688329682405815189927645711445650311717571248957446942563460243445594420475200 - square!(var1, 6); - // 328 : 6166688329682405815189927645711445650311717571248957446942563460243445594420475231 - var1.mul_assign(&var5); - // 329 : 197334026549836986086077684662766260809974962279966638302162030727790259021455207392 - square!(var1, 5); - // 334 : 197334026549836986086077684662766260809974962279966638302162030727790259021455207423 - var1.mul_assign(&var5); - // 335 : 6314688849594783554754485909208520345919198792958932425669184983289288288686566637536 - square!(var1, 5); - // 340 : 6314688849594783554754485909208520345919198792958932425669184983289288288686566637567 - var1.mul_assign(&var5); - // 341 : 101035021593516536876071774547336325534707180687342918810706959732628612618985066201072 - square!(var1, 4); - // 345 : 101035021593516536876071774547336325534707180687342918810706959732628612618985066201085 - var1.mul_assign(&var4); - // 346 : 808280172748132295008574196378690604277657445498743350485655677861028900951880529608680 - square!(var1, 3); - // 349 : 808280172748132295008574196378690604277657445498743350485655677861028900951880529608683 - var1.mul_assign(&var9); - // 350 : 206919724223521867522194994272944794695080306047678297724327853532423398643681415579822848 - square!(var1, 8); - // 358 : 206919724223521867522194994272944794695080306047678297724327853532423398643681415579822869 - var1.mul_assign(&var3); - // 359 : 26485724700610799042840959266936933720970279174102822108713965252150195026391221194217327232 - square!(var1, 7); - // 366 : 26485724700610799042840959266936933720970279174102822108713965252150195026391221194217327263 - var1.mul_assign(&var5); - // 367 : 847543190419545569370910696541981879071048933571290307478846888068806240844519078214954472416 - square!(var1, 5); - // 372 : 847543190419545569370910696541981879071048933571290307478846888068806240844519078214954472447 - var1.mul_assign(&var5); - // 373 : 27121382093425458219869142289343420130273565874281289839323100418201799707024610502878543118304 - square!(var1, 5); - // 378 : 27121382093425458219869142289343420130273565874281289839323100418201799707024610502878543118335 - var1.mul_assign(&var5); - // 379 : 433942113494807331517906276629494722084377053988500637429169606691228795312393768046056689893360 - square!(var1, 4); - // 383 : 433942113494807331517906276629494722084377053988500637429169606691228795312393768046056689893375 - var1.mul_assign(&var8); - // 384 : 6943073815916917304286500426071915553350032863816010198866713707059660724998300288736907038294000 - square!(var1, 4); - // 388 : 6943073815916917304286500426071915553350032863816010198866713707059660724998300288736907038294007 - var1.mul_assign(&var7); - // 389 : 888713448437365414948672054537205190828804206568449305454939354503636572799782436958324100901632896 - square!(var1, 7); - // 396 : 888713448437365414948672054537205190828804206568449305454939354503636572799782436958324100901632927 - var1.mul_assign(&var5); - // 397 : 28438830349995693278357505745190566106521734610190377774558059344116370329593037982666371228852253664 - square!(var1, 5); - // 402 : 28438830349995693278357505745190566106521734610190377774558059344116370329593037982666371228852253693 - var1.mul_assign(&var6); - // 403 : 910042571199862184907440183846098115408695507526092088785857899011723850546977215445323879323272118176 - square!(var1, 5); - // 408 : 910042571199862184907440183846098115408695507526092088785857899011723850546977215445323879323272118207 - var1.mul_assign(&var5); - // 409 : 29121362278395589917038085883075139693078256240834946841147452768375163217503270894250364138344707782624 - square!(var1, 5); - // 414 : 29121362278395589917038085883075139693078256240834946841147452768375163217503270894250364138344707782655 - var1.mul_assign(&var5); - // 415 : 931883592908658877345218748258404470178504199706718298916718488588005222960104668616011652427030649044960 - square!(var1, 5); - // 420 : 931883592908658877345218748258404470178504199706718298916718488588005222960104668616011652427030649044991 - var1.mul_assign(&var5); - // 421 : 29820274973077084075046999944268943045712134390614985565334991634816167134723349395712372877664980769439712 - square!(var1, 5); - // 426 : 29820274973077084075046999944268943045712134390614985565334991634816167134723349395712372877664980769439743 - var1.mul_assign(&var5); - // 427 : 954248799138466690401503998216606177462788300499679538090719732314117348311147180662795932085279384622071776 - square!(var1, 5); - // 432 : 954248799138466690401503998216606177462788300499679538090719732314117348311147180662795932085279384622071807 - var1.mul_assign(&var5); - // 433 : 30535961572430934092848127942931397678809225615989745218903031434051755145956709781209469826728940307906297824 - square!(var1, 5); - // 438 : 30535961572430934092848127942931397678809225615989745218903031434051755145956709781209469826728940307906297855 - var1.mul_assign(&var5); - // 439 : 488575385158894945485570047086902362860947609855835923502448502944828082335307356499351517227663044926500765680 - square!(var1, 4); - // 443 : 488575385158894945485570047086902362860947609855835923502448502944828082335307356499351517227663044926500765693 - var1.mul_assign(&var4); - // 444 : 31268824650169276511076483013561751223100647030773499104156704188468997269459670815958497102570434875296049004352 - square!(var1, 6); - // 450 : 31268824650169276511076483013561751223100647030773499104156704188468997269459670815958497102570434875296049004373 - var1.mul_assign(&var3); - // 451 : 500301194402708424177223728216988019569610352492375985666507267015503956311354733055335953641126958004736784069968 - square!(var1, 4); - // 455 : 500301194402708424177223728216988019569610352492375985666507267015503956311354733055335953641126958004736784069973 - var1.mul_assign(&var2); - // 456 : 1000602388805416848354447456433976039139220704984751971333014534031007912622709466110671907282253916009473568139946 - var1.square() -} - -#[allow(clippy::cognitive_complexity)] -/// addchain for 1001205140483106588246484290269935788605945006208159541241399033561623546780709821462541004956387089373434649096260670658193992783731681621012512651314777238193313314641988297376025498093520728838658813979860931248214124593092835 -/// Bos-Coster (win=4) : 895 links, 17 variables -/// Addition chain implementing exponentiation by (p**2 - 9) // 16. -pub fn chain_p2m9div16(var0: &Fp2) -> Fp2 { - let mut var1 = var0.square(); - //Self::sqr(var1, var0); /* 0 : 2 */ - let var2 = var1 * var0; - //Self::mul(&mut var2, var1, var0); /* 1 : 3 */ - let var15 = var2 * var1; - //Self::mul(&mut var15, &var2, var1); /* 2 : 5 */ - let var3 = var15 * var1; - //Self::mul(&mut var3, &var15, var1); /* 3 : 7 */ - let var14 = var3 * var1; - //Self::mul(&mut var14, &var3, var1); /* 4 : 9 */ - let var13 = var14 * var1; - //Self::mul(&mut var13, &var14, var1); /* 5 : 11 */ - let var5 = var13 * var1; - //Self::mul(&mut var5, &var13, var1); /* 6 : 13 */ - let var10 = var5 * var1; - //Self::mul(&mut var10, &var5, var1); /* 7 : 15 */ - let var9 = var10 * var1; - //Self::mul(&mut var9, &var10, var1); /* 8 : 17 */ - let var16 = var9 * var1; - //Self::mul(&mut var16, &var9, var1); /* 9 : 19 */ - let var4 = var16 * var1; - //Self::mul(&mut var4, &var16, var1); /* 10 : 21 */ - let var7 = var4 * var1; - //Self::mul(&mut var7, &var4, var1); /* 11 : 23 */ - let var6 = var7 * var1; - //Self::mul(&mut var6, &var7, var1); /* 12 : 25 */ - let var12 = var6 * var1; - //Self::mul(&mut var12, &var6, var1); /* 13 : 27 */ - let var8 = var12 * var1; - //Self::mul(&mut var8, &var12, var1); /* 14 : 29 */ - let var11 = var8 * var1; - //Self::mul(&mut var11, &var8, var1); /* 15 : 31 */ - var1 = var4.square(); - //Self::sqr(var1, &var4); /* 16 : 42 */ - // 17 : 168 - square!(var1, 2); - // 19 : 169 - var1.mul_assign(var0); - // 20 : 86528 - square!(var1, 9); - // 29 : 86555 - var1.mul_assign(&var12); - // 30 : 1384880 - square!(var1, 4); - // 34 : 1384893 - var1.mul_assign(&var5); - // 35 : 88633152 - square!(var1, 6); - // 41 : 88633161 - var1.mul_assign(&var14); - // 42 : 1418130576 - square!(var1, 4); - // 46 : 1418130583 - var1.mul_assign(&var3); - // 47 : 45380178656 - square!(var1, 5); - // 52 : 45380178659 - var1.mul_assign(&var2); - // 53 : 11617325736704 - square!(var1, 8); - // 61 : 11617325736717 - var1.mul_assign(&var5); - // 62 : 185877211787472 - square!(var1, 4); - // 66 : 185877211787479 - var1.mul_assign(&var3); - // 67 : 2974035388599664 - square!(var1, 4); - // 71 : 2974035388599679 - var1.mul_assign(&var10); - // 72 : 761353059481517824 - square!(var1, 8); - // 80 : 761353059481517853 - var1.mul_assign(&var8); - // 81 : 48726595806817142592 - square!(var1, 6); - // 87 : 48726595806817142603 - var1.mul_assign(&var13); - // 88 : 779625532909074281648 - square!(var1, 4); - // 92 : 779625532909074281661 - var1.mul_assign(&var5); - // 93 : 6237004263272594253288 - square!(var1, 3); - // 96 : 6237004263272594253289 - var1.mul_assign(var0); - // 97 : 399168272849446032210496 - square!(var1, 6); - // 103 : 399168272849446032210511 - var1.mul_assign(&var10); - // 104 : 102187077849458184245890816 - square!(var1, 8); - // 112 : 102187077849458184245890845 - var1.mul_assign(&var8); - // 113 : 6539972982365323791737014080 - square!(var1, 6); - // 119 : 6539972982365323791737014101 - var1.mul_assign(&var4); - // 120 : 1674233083485522890684675609856 - square!(var1, 8); - // 128 : 1674233083485522890684675609873 - var1.mul_assign(&var9); - // 129 : 53575458671536732501909619515936 - square!(var1, 5); - // 134 : 53575458671536732501909619515951 - var1.mul_assign(&var10); - // 135 : 3428829354978350880122215649020864 - square!(var1, 6); - // 141 : 3428829354978350880122215649020873 - var1.mul_assign(&var14); - // 142 : 109722539359307228163910900768667936 - square!(var1, 5); - // 147 : 109722539359307228163910900768667951 - var1.mul_assign(&var10); - // 148 : 438890157437228912655643603074671804 - square!(var1, 2); - // 150 : 438890157437228912655643603074671805 - var1.mul_assign(var0); - // 151 : 28088970075982650409961190596778995520 - square!(var1, 6); - // 157 : 28088970075982650409961190596778995535 - var1.mul_assign(&var10); - // 158 : 3595388169725779252475032396387711428480 - square!(var1, 7); - // 165 : 3595388169725779252475032396387711428491 - var1.mul_assign(&var13); - // 166 : 57526210715612468039600518342203382855856 - square!(var1, 4); - // 170 : 57526210715612468039600518342203382855863 - var1.mul_assign(&var3); - // 171 : 3681677485799197954534433173901016502775232 - square!(var1, 6); - // 177 : 3681677485799197954534433173901016502775241 - var1.mul_assign(&var14); - // 178 : 471254718182297338180407446259330112355230848 - square!(var1, 7); - // 185 : 471254718182297338180407446259330112355230855 - var1.mul_assign(&var3); - // 186 : 15080150981833514821773038280298563595367387360 - square!(var1, 5); - // 191 : 15080150981833514821773038280298563595367387365 - var1.mul_assign(&var15); - // 192 : 1930259325674689897186948899878216140207025582720 - square!(var1, 7); - // 199 : 1930259325674689897186948899878216140207025582727 - var1.mul_assign(&var3); - // 200 : 61768298421590076709982364796102916486624818647264 - square!(var1, 5); - // 205 : 61768298421590076709982364796102916486624818647271 - var1.mul_assign(&var3); - // 206 : 63250737583708238551021941551209386482303814294805504 - square!(var1, 10); - // 216 : 63250737583708238551021941551209386482303814294805521 - var1.mul_assign(&var9); - // 217 : 506005900669665908408175532409675091858430514358444168 - square!(var1, 3); - // 220 : 506005900669665908408175532409675091858430514358444173 - var1.mul_assign(&var15); - // 221 : 16192188821429309069061617037109602939469776459470213536 - square!(var1, 5); - // 226 : 16192188821429309069061617037109602939469776459470213549 - var1.mul_assign(&var5); - // 227 : 4145200338285903121679773961500058352504262773624374668544 - square!(var1, 8); - // 235 : 4145200338285903121679773961500058352504262773624374668569 - var1.mul_assign(&var6); - // 236 : 132646410825148899893752766768001867280136408755979989394208 - square!(var1, 5); - // 241 : 132646410825148899893752766768001867280136408755979989394231 - var1.mul_assign(&var7); - // 242 : 8489370292809529593200177073152119505928730160382719321230784 - square!(var1, 6); - // 248 : 8489370292809529593200177073152119505928730160382719321230795 - var1.mul_assign(&var13); - // 249 : 543319698739809893964811332681735648379438730264494036558770880 - square!(var1, 6); - // 255 : 543319698739809893964811332681735648379438730264494036558770895 - var1.mul_assign(&var10); - // 256 : 34772460719347833213747925291631081496284078736927618339761337280 - square!(var1, 6); - // 262 : 34772460719347833213747925291631081496284078736927618339761337289 - var1.mul_assign(&var14); - // 263 : 4450874972076522651359734437328778431524362078326735147489451172992 - square!(var1, 7); - // 270 : 4450874972076522651359734437328778431524362078326735147489451173011 - var1.mul_assign(&var16); - // 271 : 142427999106448724843511501994520909808779586506455524719662437536352 - square!(var1, 5); - // 276 : 142427999106448724843511501994520909808779586506455524719662437536361 - var1.mul_assign(&var14); - // 277 : 9115391942812718389984736127649338227761893536413153582058396002327104 - square!(var1, 6); - // 283 : 9115391942812718389984736127649338227761893536413153582058396002327119 - var1.mul_assign(&var10); - // 284 : 583385084340013976959023112169557646576761186330441829251737344148935616 - square!(var1, 6); - // 290 : 583385084340013976959023112169557646576761186330441829251737344148935633 - var1.mul_assign(&var9); - // 291 : 18668322698880447262688739589425844690456357962574138536055595012765940256 - square!(var1, 5); - // 296 : 18668322698880447262688739589425844690456357962574138536055595012765940271 - var1.mul_assign(&var10); - // 297 : 74673290795521789050754958357703378761825431850296554144222380051063761084 - square!(var1, 2); - // 299 : 74673290795521789050754958357703378761825431850296554144222380051063761085 - var1.mul_assign(var0); - // 300 : 19116362443653577996993269339572064963027310553675917860920929293072322837760 - square!(var1, 8); - // 308 : 19116362443653577996993269339572064963027310553675917860920929293072322837765 - var1.mul_assign(&var15); - // 309 : 2446894392787657983615138475465224315267495750870517486197878949513257323233920 - square!(var1, 7); - // 316 : 2446894392787657983615138475465224315267495750870517486197878949513257323233925 - var1.mul_assign(&var15); - // 317 : 39150310284602527737842215607443589044279932013928279779166063192212117171742800 - square!(var1, 4); - // 321 : 39150310284602527737842215607443589044279932013928279779166063192212117171742803 - var1.mul_assign(&var2); - // 322 : 5011239716429123550443803597752779397667831297782819811733256088603150997983078784 - square!(var1, 7); - // 329 : 5011239716429123550443803597752779397667831297782819811733256088603150997983078795 - var1.mul_assign(&var13); - // 330 : 320719341851463907228403430256177881450741203058100467950928389670601663870917042880 - square!(var1, 6); - // 336 : 320719341851463907228403430256177881450741203058100467950928389670601663870917042895 - var1.mul_assign(&var10); - // 337 : 5131509469623422515654454884098846103211859248929607487214854234729626621934672686320 - square!(var1, 4); - // 341 : 5131509469623422515654454884098846103211859248929607487214854234729626621934672686333 - var1.mul_assign(&var5); - // 342 : 656833212111798082003770225164652301211117983862989758363501342045392207607638103850624 - square!(var1, 7); - // 349 : 656833212111798082003770225164652301211117983862989758363501342045392207607638103850635 - var1.mul_assign(&var13); - // 350 : 42037325575155077248241294410537747277511550967231344535264085890905101286888838646440640 - square!(var1, 6); - // 356 : 42037325575155077248241294410537747277511550967231344535264085890905101286888838646440667 - var1.mul_assign(&var12); - // 357 : 1345194418404962471943721421137207912880369630951403025128450748508963241180442836686101344 - square!(var1, 5); - // 362 : 1345194418404962471943721421137207912880369630951403025128450748508963241180442836686101367 - var1.mul_assign(&var7); - // 363 : 43046221388958799102199085476390653212171828190444896804110423952286823717774170773955243744 - square!(var1, 5); - // 368 : 43046221388958799102199085476390653212171828190444896804110423952286823717774170773955243749 - var1.mul_assign(&var15); - // 369 : 5509916337786726285081482940978003611157994008376946790926134265892713435875093859066271199872 - square!(var1, 7); - // 376 : 5509916337786726285081482940978003611157994008376946790926134265892713435875093859066271199899 - var1.mul_assign(&var12); - // 377 : 176317322809175241122607454111296115557055808268062297309636296508566829948003003490120678396768 - square!(var1, 5); - // 382 : 176317322809175241122607454111296115557055808268062297309636296508566829948003003490120678396791 - var1.mul_assign(&var7); - // 383 : 5642154329893607715923438531561475697825785864577993513908361488274138558336096111683861708697312 - square!(var1, 5); - // 388 : 5642154329893607715923438531561475697825785864577993513908361488274138558336096111683861708697333 - var1.mul_assign(&var4); - // 389 : 90274469278297723454775016504983611165212573833247896222533783812386216933377537786941787339157328 - square!(var1, 4); - // 393 : 90274469278297723454775016504983611165212573833247896222533783812386216933377537786941787339157331 - var1.mul_assign(&var2); - // 394 : 5777566033811054301105601056318951114573604725327865358242162163992717883736162418364274389706069184 - square!(var1, 6); - // 400 : 5777566033811054301105601056318951114573604725327865358242162163992717883736162418364274389706069189 - var1.mul_assign(&var15); - // 401 : 369764226163907475270758467604412871332710702420983382927498378495533944559114394775313560941188428096 - square!(var1, 6); - // 407 : 369764226163907475270758467604412871332710702420983382927498378495533944559114394775313560941188428105 - var1.mul_assign(&var14); - // 408 : 5916227618622519604332135481670605941323371238735734126839974055928543112945830316405016975059014849680 - square!(var1, 4); - // 412 : 5916227618622519604332135481670605941323371238735734126839974055928543112945830316405016975059014849683 - var1.mul_assign(&var2); - // 413 : 94659641897960313669314167706729695061173939819771746029439584894856689807133285062480271600944237594928 - square!(var1, 4); - // 417 : 94659641897960313669314167706729695061173939819771746029439584894856689807133285062480271600944237594931 - var1.mul_assign(&var2); - // 418 : 24232868325877840299344426932922801935660528593861566983536533733083312590626120975994949529841724824302336 - square!(var1, 8); - // 426 : 24232868325877840299344426932922801935660528593861566983536533733083312590626120975994949529841724824302345 - var1.mul_assign(&var14); - // 427 : 775451786428090889579021661853529661941136915003570143473169079458666002900035871231838384954935194377675040 - square!(var1, 5); - // 432 : 775451786428090889579021661853529661941136915003570143473169079458666002900035871231838384954935194377675055 - var1.mul_assign(&var10); - // 433 : 49628914331397816933057386358625898364232762560228489182282821085354624185602295758837656637115852440171203520 - square!(var1, 6); - // 439 : 49628914331397816933057386358625898364232762560228489182282821085354624185602295758837656637115852440171203527 - var1.mul_assign(&var3); - // 440 : 1588125258604730141857836363476028747655448401927311653833050274731347973939273464282805012387707278085478512864 - square!(var1, 5); - // 445 : 1588125258604730141857836363476028747655448401927311653833050274731347973939273464282805012387707278085478512879 - var1.mul_assign(&var10); - // 446 : 6504961059244974661049697744797813750396716654294268534100173925299601301255264109702369330740049011038119988752384 - square!(var1, 12); - // 458 : 6504961059244974661049697744797813750396716654294268534100173925299601301255264109702369330740049011038119988752401 - var1.mul_assign(&var9); - // 459 : 104079376947919594576795163916765020006347466468708296545602782804793620820084225755237909291840784176609919820038416 - square!(var1, 4); - // 463 : 104079376947919594576795163916765020006347466468708296545602782804793620820084225755237909291840784176609919820038429 - var1.mul_assign(&var5); - // 464 : 3330540062333427026457445245336480640203118926998665489459289049753395866242695224167613097338905093651517434241229728 - square!(var1, 5); - // 469 : 3330540062333427026457445245336480640203118926998665489459289049753395866242695224167613097338905093651517434241229741 - var1.mul_assign(&var5); - // 470 : 213154563989339329693276495701534760972999611327914591325394499184217335439532494346727238229689925993697115791438703424 - square!(var1, 6); - // 476 : 213154563989339329693276495701534760972999611327914591325394499184217335439532494346727238229689925993697115791438703427 - var1.mul_assign(&var2); - // 477 : 109135136762541736802957565799185797618175800999892270758601983582319275745040637105524345973601242108772923285216616154624 - square!(var1, 9); - // 486 : 109135136762541736802957565799185797618175800999892270758601983582319275745040637105524345973601242108772923285216616154649 - var1.mul_assign(&var6); - // 487 : 3492324376401335577694642105573945523781625631996552664275263474634216823841300387376779071155239747480733545126931716948768 - square!(var1, 5); - // 492 : 3492324376401335577694642105573945523781625631996552664275263474634216823841300387376779071155239747480733545126931716948793 - var1.mul_assign(&var6); - // 493 : 223508760089685476972457094756732513522024040447779370513616862376589876725843224792113860553935343838766946888123629884722752 - square!(var1, 6); - // 499 : 223508760089685476972457094756732513522024040447779370513616862376589876725843224792113860553935343838766946888123629884722755 - var1.mul_assign(&var2); - // 500 : 14304560645739870526237254064430880865409538588657879712871479192101752110453966386695287075451862005681084600839912312622256320 - square!(var1, 6); - // 506 : 14304560645739870526237254064430880865409538588657879712871479192101752110453966386695287075451862005681084600839912312622256323 - var1.mul_assign(&var2); - // 507 : 7323935050618813709433474080988611003089683757392834412990197346356097080552430789987986982631353346908715315630035104062595237376 - square!(var1, 9); - // 516 : 7323935050618813709433474080988611003089683757392834412990197346356097080552430789987986982631353346908715315630035104062595237399 - var1.mul_assign(&var7); - // 517 : 937463686479208154807484682366542208395479520946282804862745260333580426310711141118462333776813228404315560400644493320012190387072 - square!(var1, 7); - // 524 : 937463686479208154807484682366542208395479520946282804862745260333580426310711141118462333776813228404315560400644493320012190387087 - var1.mul_assign(&var10); - // 525 : 59997675934669321907679019671458701337310689340562099511215696661349147283885513031581589361716046617876195865641247572480780184773568 - square!(var1, 6); - // 531 : 59997675934669321907679019671458701337310689340562099511215696661349147283885513031581589361716046617876195865641247572480780184773593 - var1.mul_assign(&var6); - // 532 : 1919925629909418301045728629486678442793942058897987184358902293163172713084336417010610859574913491772038267700519922319384965912754976 - square!(var1, 5); - // 537 : 1919925629909418301045728629486678442793942058897987184358902293163172713084336417010610859574913491772038267700519922319384965912754985 - var1.mul_assign(&var14); - // 538 : 245750480628405542533853264574294840677624583538942359597939493524886107274795061377358190025588926946820898265666550056881275636832638080 - square!(var1, 7); - // 545 : 245750480628405542533853264574294840677624583538942359597939493524886107274795061377358190025588926946820898265666550056881275636832638103 - var1.mul_assign(&var7); - // 546 : 983001922513622170135413058297179362710498334155769438391757974099544429099180245509432760102355707787283593062666200227525102547330552412 - square!(var1, 2); - // 548 : 983001922513622170135413058297179362710498334155769438391757974099544429099180245509432760102355707787283593062666200227525102547330552413 - var1.mul_assign(var0); - // 549 : 251648492163487275554665742924077916853887573543876976228290041369483373849390142850414786586203061193544599824042547258246426252116621417728 - square!(var1, 8); - // 557 : 251648492163487275554665742924077916853887573543876976228290041369483373849390142850414786586203061193544599824042547258246426252116621417739 - var1.mul_assign(&var13); - // 558 : 4026375874615796408874651886785246669662201176702031619652640661911733981590242285606636585379248979096713597184680756131942820033865942683824 - square!(var1, 4); - // 562 : 4026375874615796408874651886785246669662201176702031619652640661911733981590242285606636585379248979096713597184680756131942820033865942683829 - var1.mul_assign(&var15); - // 563 : 515376111950821940335955441508511573716761750617860047315538004724701949643551012557649482928543869324379340439639136784888680964334840663530112 - square!(var1, 7); - // 570 : 515376111950821940335955441508511573716761750617860047315538004724701949643551012557649482928543869324379340439639136784888680964334840663530119 - var1.mul_assign(&var3); - // 571 : 131936284659410416726004593026178962871491008158172172112777729209523699108749059214758267629707230547041111152547619016931502326869719209863710464 - square!(var1, 8); - // 579 : 131936284659410416726004593026178962871491008158172172112777729209523699108749059214758267629707230547041111152547619016931502326869719209863710473 - var1.mul_assign(&var14); - // 580 : 16887844436404533340928587907350907247550849044246038030435549338819033485919879579489058256602525510021262227526095234167232297839324058862554940544 - square!(var1, 7); - // 587 : 16887844436404533340928587907350907247550849044246038030435549338819033485919879579489058256602525510021262227526095234167232297839324058862554940557 - var1.mul_assign(&var5); - // 588 : 17293152702878242141110874017127329021492069421307942943166002522950690289581956689396795654760986122261772520986721519787245872987467836275256259130368 - square!(var1, 10); - // 598 : 17293152702878242141110874017127329021492069421307942943166002522950690289581956689396795654760986122261772520986721519787245872987467836275256259130377 - var1.mul_assign(&var14); - // 599 : 1106761772984207497031095937096149057375492442963708348362624161468844178533245228121394921904703111824753441343150177266383735871197941521616400584344128 - square!(var1, 6); - // 605 : 1106761772984207497031095937096149057375492442963708348362624161468844178533245228121394921904703111824753441343150177266383735871197941521616400584344139 - var1.mul_assign(&var13); - // 606 : 70832753470989279809990139974153539672031516349677334295207946334006027426127694599769275001900999156784220245961611345048559095756668257383449637398024896 - square!(var1, 6); - // 612 : 70832753470989279809990139974153539672031516349677334295207946334006027426127694599769275001900999156784220245961611345048559095756668257383449637398024909 - var1.mul_assign(&var5); - // 613 : 4533296222143313907839368958345826539010017046379349394893308565376385755272172454385233600121663946034190095741543126083107782128426768472540776793473594176 - square!(var1, 6); - // 619 : 4533296222143313907839368958345826539010017046379349394893308565376385755272172454385233600121663946034190095741543126083107782128426768472540776793473594207 - var1.mul_assign(&var11); - // 620 : 145065479108586045050859806667066449248320545484139180636585874092044344168709518540327475203893246273094083063729380034659449028109656591121304857391155014624 - square!(var1, 5); - // 625 : 145065479108586045050859806667066449248320545484139180636585874092044344168709518540327475203893246273094083063729380034659449028109656591121304857391155014649 - var1.mul_assign(&var6); - // 626 : 18568381325899013766510055253384505503785029821969815121482991883781676053594818373161916826098335522956042632157360644436409475598036043663527021746067841875072 - square!(var1, 7); - // 633 : 18568381325899013766510055253384505503785029821969815121482991883781676053594818373161916826098335522956042632157360644436409475598036043663527021746067841875087 - var1.mul_assign(&var10); - // 634 : 594188202428768440528321768108304176121120954303034083887455740281013633715034187941181338435146736734593364229035540621965103219137153397232864695874170940002784 - square!(var1, 5); - // 639 : 594188202428768440528321768108304176121120954303034083887455740281013633715034187941181338435146736734593364229035540621965103219137153397232864695874170940002797 - var1.mul_assign(&var5); - // 640 : 76056089910882360387625186317862934543503482150788362737594334755969745115524376056471211319698782302027950621316549199611533212049555634845806681071893880320358016 - square!(var1, 7); - // 647 : 76056089910882360387625186317862934543503482150788362737594334755969745115524376056471211319698782302027950621316549199611533212049555634845806681071893880320358047 - var1.mul_assign(&var11); - // 648 : 2433794877148235532404005962171613905392111428825227607603018712191031843696780033807078762230361033664894419882129574387569062785585780315065813794300604170251457504 - square!(var1, 5); - // 653 : 2433794877148235532404005962171613905392111428825227607603018712191031843696780033807078762230361033664894419882129574387569062785585780315065813794300604170251457511 - var1.mul_assign(&var3); - // 654 : 623051488549948296295425526315933159780380525779258267546372790320904151986375688654612163130972424618212971489825171043217680073109959760656848331340954667584373122816 - square!(var1, 8); - // 662 : 623051488549948296295425526315933159780380525779258267546372790320904151986375688654612163130972424618212971489825171043217680073109959760656848331340954667584373122843 - var1.mul_assign(&var12); - // 663 : 39875295267196690962907233684219722225944353649872529122967858580537865727128044073895178440382235175565630175348810946765931524679037424682038293205821098725399879861952 - square!(var1, 6); - // 669 : 39875295267196690962907233684219722225944353649872529122967858580537865727128044073895178440382235175565630175348810946765931524679037424682038293205821098725399879861981 - var1.mul_assign(&var8); - // 670 : 2552018897100588221626062955790062222460438633591841863869942949154423406536194820729291420184463051236200331222323900593019617579458395179650450765172550318425592311166784 - square!(var1, 6); - // 676 : 2552018897100588221626062955790062222460438633591841863869942949154423406536194820729291420184463051236200331222323900593019617579458395179650450765172550318425592311166787 - var1.mul_assign(&var2); - // 677 : 326658418828875292368136058341127964474936145099755758575352697491766196036632937053349301783611270558233642396457459275906511050170674582995257697942086440758475815829348736 - square!(var1, 7); - // 684 : 326658418828875292368136058341127964474936145099755758575352697491766196036632937053349301783611270558233642396457459275906511050170674582995257697942086440758475815829348747 - var1.mul_assign(&var13); - // 685 : 41812277610096037423121415467664379452791826572768737097645145278946073092689015942828710628302242631453906226746554787316033414421846346623392985336587064417084904426156639616 - square!(var1, 7); - // 692 : 41812277610096037423121415467664379452791826572768737097645145278946073092689015942828710628302242631453906226746554787316033414421846346623392985336587064417084904426156639627 - var1.mul_assign(&var13); - // 693 : 2675985767046146395079770589930520284978676900657199174249289297852548677932097020341037480211343528413049998511779506388226138522998166183897151061541572122693433883274024936128 - square!(var1, 6); - // 699 : 2675985767046146395079770589930520284978676900657199174249289297852548677932097020341037480211343528413049998511779506388226138522998166183897151061541572122693433883274024936131 - var1.mul_assign(&var2); - // 700 : 85631544545476684642552658877776649119317660821030373575977257531281557693827104650913199366762992909217599952376944204423236432735941317884708833969330307926189884264768797956192 - square!(var1, 5); - // 705 : 85631544545476684642552658877776649119317660821030373575977257531281557693827104650913199366762992909217599952376944204423236432735941317884708833969330307926189884264768797956199 - var1.mul_assign(&var3); - // 706 : 87686701614568125073973922690843288698181284680735102541800711712032315078478955162535116151565304739038822351233990865329394107121603909513941845984594235316418441487123249107147776 - square!(var1, 10); - // 716 : 87686701614568125073973922690843288698181284680735102541800711712032315078478955162535116151565304739038822351233990865329394107121603909513941845984594235316418441487123249107147803 - var1.mul_assign(&var12); - // 717 : 1402987225833090001183582763053492619170900554891761640668811387392517041255663282600561858425044875824621157619743853845270305713945662552223069535753507765062695063793971985714364848 - square!(var1, 4); - // 721 : 1402987225833090001183582763053492619170900554891761640668811387392517041255663282600561858425044875824621157619743853845270305713945662552223069535753507765062695063793971985714364849 - var1.mul_assign(var0); - // 722 : 718329459626542080605994374683388221015501084104581960022431430344968725122899600691487671513622976422206032701308853168778396525540179226738211602305795975712099872662513656685754802688 - square!(var1, 9); - // 731 : 718329459626542080605994374683388221015501084104581960022431430344968725122899600691487671513622976422206032701308853168778396525540179226738211602305795975712099872662513656685754802705 - var1.mul_assign(&var9); - // 732 : 45973085416098693158783639979736846144992069382693245441435611542077998407865574444255210976871870491021186092883766602801817377634571470511245542547570942445574391850400874027888307373120 - square!(var1, 6); - // 738 : 45973085416098693158783639979736846144992069382693245441435611542077998407865574444255210976871870491021186092883766602801817377634571470511245542547570942445574391850400874027888307373135 - var1.mul_assign(&var10); - // 739 : 5884554933260632724324305917406316306558984880984735416503758277385983796206793528864667005039599422850711819889122125158632624337225148225439429446089080633033522156851311875569703343761280 - square!(var1, 7); - // 746 : 5884554933260632724324305917406316306558984880984735416503758277385983796206793528864667005039599422850711819889122125158632624337225148225439429446089080633033522156851311875569703343761311 - var1.mul_assign(&var11); - // 747 : 188305757864340247178377789357002121809887516191511533328120264876351481478617392923669344161267181531222778236451908005076243978791204743214061742274850580257072709019241980018230507000361952 - square!(var1, 5); - // 752 : 188305757864340247178377789357002121809887516191511533328120264876351481478617392923669344161267181531222778236451908005076243978791204743214061742274850580257072709019241980018230507000361973 - var1.mul_assign(&var4); - // 753 : 3012892125829443954854044629712033948958200259064184533249924238021623703657878286778709506580274904499564451783230528081219903660659275891424987876397609284113163344307871680291688112005791568 - square!(var1, 4); - // 757 : 3012892125829443954854044629712033948958200259064184533249924238021623703657878286778709506580274904499564451783230528081219903660659275891424987876397609284113163344307871680291688112005791583 - var1.mul_assign(&var10); - // 758 : 385650192106168826221317712603140345466649633160215620255990302466767834068208420707674816842275187775944249828253507594396147668564387314102398448178893988366484908071407575077336078336741322624 - square!(var1, 7); - // 765 : 385650192106168826221317712603140345466649633160215620255990302466767834068208420707674816842275187775944249828253507594396147668564387314102398448178893988366484908071407575077336078336741322653 - var1.mul_assign(&var8); - // 766 : 12340806147397402439082166803300491054932788261126899848191689678936570690182669462645594138952806008830215994504112243020676725394060394051276750341724607627727517058285042402474754506775722324896 - square!(var1, 5); - // 771 : 12340806147397402439082166803300491054932788261126899848191689678936570690182669462645594138952806008830215994504112243020676725394060394051276750341724607627727517058285042402474754506775722324917 - var1.mul_assign(&var4); - // 772 : 394905796716716878050629337705615713757849224356060795142134069725970262085845422804659012446489792282566911824131591776661655212609932609640856010935187444087280545865121356879192144216823114397344 - square!(var1, 5); - // 777 : 394905796716716878050629337705615713757849224356060795142134069725970262085845422804659012446489792282566911824131591776661655212609932609640856010935187444087280545865121356879192144216823114397365 - var1.mul_assign(&var4); - // 778 : 12636985494934940097620138806579702840251175179393945444548290231231048386747053529749088398287673353042141178372210936853172966803517843508507392349925998210792977467683883420134148614938339660715680 - square!(var1, 5); - // 783 : 12636985494934940097620138806579702840251175179393945444548290231231048386747053529749088398287673353042141178372210936853172966803517843508507392349925998210792977467683883420134148614938339660715697 - var1.mul_assign(&var9); - // 784 : 202191767918959041561922220905275245444018802870303127112772643699696774187952856475985414372602773648674258853955374989650767468856285496136118277598815971372687639482942134722146377839013434571451152 - square!(var1, 4); - // 788 : 202191767918959041561922220905275245444018802870303127112772643699696774187952856475985414372602773648674258853955374989650767468856285496136118277598815971372687639482942134722146377839013434571451165 - var1.mul_assign(&var5); - // 789 : 12940273146813378659963022137937615708417203383699400135217449196780593548028982814463066519846577513515152566653143999337649118006802271752711569766324222167852008926908296622217368181696859812572874560 - square!(var1, 6); - // 795 : 12940273146813378659963022137937615708417203383699400135217449196780593548028982814463066519846577513515152566653143999337649118006802271752711569766324222167852008926908296622217368181696859812572874589 - var1.mul_assign(&var8); - // 796 : 25880546293626757319926044275875231416834406767398800270434898393561187096057965628926133039693155027030305133306287998675298236013604543505423139532648444335704017853816593244434736363393719625145749178 - var1 = var1.square(); - // 797 : 25880546293626757319926044275875231416834406767398800270434898393561187096057965628926133039693155027030305133306287998675298236013604543505423139532648444335704017853816593244434736363393719625145749179 - var1.mul_assign(var0); - // 798 : 1656354962792112468475266833656014810677402033113523217307833497187915974147709800251272514540361921729939528531602431915219087104870690784347080930089500437485057142644261967643823127257198056009327947456 - square!(var1, 6); - // 804 : 1656354962792112468475266833656014810677402033113523217307833497187915974147709800251272514540361921729939528531602431915219087104870690784347080930089500437485057142644261967643823127257198056009327947463 - var1.mul_assign(&var3); - // 805 : 1696107481899123167718673237663759166133659681908247774523221501120425957527254835457303054889330607851458077216360890281184345195387587363171410872411648447984698514067724254867274882311370809353551818202112 - square!(var1, 10); - // 815 : 1696107481899123167718673237663759166133659681908247774523221501120425957527254835457303054889330607851458077216360890281184345195387587363171410872411648447984698514067724254867274882311370809353551818202135 - var1.mul_assign(&var7); - // 816 : 108550878841543882733995087210480586632554219642127857569486176071707261281744309469267395512917158902493316941847096977995798092504805591242970295834345500671020704900334352311505592467927731798627316364936640 - square!(var1, 6); - // 822 : 108550878841543882733995087210480586632554219642127857569486176071707261281744309469267395512917158902493316941847096977995798092504805591242970295834345500671020704900334352311505592467927731798627316364936661 - var1.mul_assign(&var4); - // 823 : 6947256245858808494975685581470757544483470057096182884447115268589264722031635806033113312826698169759572284278214206591731077920307557839550098933398112042945325113621398547936357917947374835112148247355946304 - square!(var1, 6); - // 829 : 6947256245858808494975685581470757544483470057096182884447115268589264722031635806033113312826698169759572284278214206591731077920307557839550098933398112042945325113621398547936357917947374835112148247355946329 - var1.mul_assign(&var6); - // 830 : 444624399734963743678443877214128482846942083654155704604615377189712942210024691586119252020908682864612626193805709221870788986899683701731206331737479170748500807271769507067926906748631989447177487830780565056 - square!(var1, 6); - // 836 : 444624399734963743678443877214128482846942083654155704604615377189712942210024691586119252020908682864612626193805709221870788986899683701731206331737479170748500807271769507067926906748631989447177487830780565069 - var1.mul_assign(&var5); - // 837 : 28455961583037679595420408141704222902204293353865965094695384140141628301441580261511632129338155703335208076403565390199730495161579756910797205231198666927904051665393248452347322031912447324619359221169956164416 - square!(var1, 6); - // 843 : 28455961583037679595420408141704222902204293353865965094695384140141628301441580261511632129338155703335208076403565390199730495161579756910797205231198666927904051665393248452347322031912447324619359221169956164437 - var1.mul_assign(&var4); - // 844 : 238705906983162543355580399100765177871214152862586865721082456961065184302499251714358569373223087638243353151383559860752580829556389241459968722180074986980751351032731127113348364375477010926880553717580063640645533696 - square!(var1, 23); - // 867 : 238705906983162543355580399100765177871214152862586865721082456961065184302499251714358569373223087638243353151383559860752580829556389241459968722180074986980751351032731127113348364375477010926880553717580063640645533703 - var1.mul_assign(&var3); - // 868 : 15277178046922402774757145542448971383757705783205559406149277245508171795359952109718948439886277608847574601688547831088165173091608911453437998219524799166768086466094792135254295320030528699320355437925124073001314156992 - square!(var1, 6); - // 874 : 15277178046922402774757145542448971383757705783205559406149277245508171795359952109718948439886277608847574601688547831088165173091608911453437998219524799166768086466094792135254295320030528699320355437925124073001314156999 - var1.mul_assign(&var3); - // 875 : 488869697501516888792228657358367084280246585062577900996776871856261497451518467511006350076360883483122387254033530594821285538931485166510015943024793573336578766915033348328137450240976918378251374013603970336042053023968 - square!(var1, 5); - // 880 : 488869697501516888792228657358367084280246585062577900996776871856261497451518467511006350076360883483122387254033530594821285538931485166510015943024793573336578766915033348328137450240976918378251374013603970336042053023971 - var1.mul_assign(&var2); - // 881 : 31287660640097080882702634070935493393935781444004985663793719798800735836897181920704406404887096542919832784258145958068562274491615050656641020353586788693541041082562134293000796815422522776208087936870654101506691393534144 - square!(var1, 6); - // 887 : 31287660640097080882702634070935493393935781444004985663793719798800735836897181920704406404887096542919832784258145958068562274491615050656641020353586788693541041082562134293000796815422522776208087936870654101506691393534151 - var1.mul_assign(&var3); - // 888 : 1001205140483106588246484290269935788605945006208159541241399033561623546780709821462541004956387089373434649096260670658193992783731681621012512651314777238193313314641988297376025498093520728838658813979860931248214124593092832 - square!(var1, 5); - // 893 : 1001205140483106588246484290269935788605945006208159541241399033561623546780709821462541004956387089373434649096260670658193992783731681621012512651314777238193313314641988297376025498093520728838658813979860931248214124593092835 - var1 * var2 -} - -/// Tests for addition chains -#[cfg(test)] -mod tests { - use super::*; - use rand_core::SeedableRng; - - const SEED: [u8; 16] = [ - 0x59, 0x62, 0xbe, 0x5d, 0x76, 0x3d, 0x31, 0x8d, 0x17, 0xdb, 0x37, 0x32, 0x54, 0x06, 0xbc, - 0xe5, - ]; - - #[test] - fn test_fp_chain() { - let mut rng = rand_xorshift::XorShiftRng::from_seed(SEED); - let p_m3_over4 = [ - 0xee7f_bfff_ffff_eaaa, - 0x07aa_ffff_ac54_ffff, - 0xd9cc_34a8_3dac_3d89, - 0xd91d_d2e1_3ce1_44af, - 0x92c6_e9ed_90d2_eb35, - 0x0680_447a_8e5f_f9a6, - ]; - - for _ in 0..32 { - let input = Fp::random(&mut rng); - assert_eq!(chain_pm3div4(&input), input.pow_vartime(&p_m3_over4)); - } - } - - #[test] - fn test_fp2_chain() { - let mut rng = rand_xorshift::XorShiftRng::from_seed(SEED); - let p_sq_m9_over16 = [ - 0xb26a_a000_01c7_18e3, - 0xd7ce_d6b1_d763_82ea, - 0x3162_c338_3621_13cf, - 0x966b_f91e_d3e7_1b74, - 0xb292_e85a_8709_1a04, - 0x11d6_8619_c861_85c7, - 0xef53_1493_3097_8ef0, - 0x050a_62cf_d16d_dca6, - 0x466e_59e4_9349_e8bd, - 0x9e2d_c90e_50e7_046b, - 0x74bd_278e_aa22_f25e, - 0x002a_437a_4b8c_35fc, - ]; - - for _ in 0..32 { - let input = Fp2::random(&mut rng); - assert_eq!( - chain_p2m9div16(&input), - input.pow_vartime_extended(&p_sq_m9_over16[..]), - ); - } - } -} diff --git a/constantine/bls12_381/src/hash_to_curve/expand_msg.rs b/constantine/bls12_381/src/hash_to_curve/expand_msg.rs deleted file mode 100644 index 7187d46b7..000000000 --- a/constantine/bls12_381/src/hash_to_curve/expand_msg.rs +++ /dev/null @@ -1,1282 +0,0 @@ -//! This module implements message expansion consistent with the -//! hash-to-curve RFC drafts 7 through 10 - -use core::{ - fmt::{self, Debug, Formatter}, - marker::PhantomData, -}; - -use digest::{BlockInput, Digest, ExtendableOutputDirty, Update, XofReader}; - -use crate::generic_array::{ - typenum::{Unsigned, U32}, - ArrayLength, GenericArray, -}; - -#[cfg(feature = "alloc")] -use alloc::vec::Vec; - -const OVERSIZE_DST_SALT: &[u8] = b"H2C-OVERSIZE-DST-"; - -/// The domain separation tag for a message expansion. -/// -/// Implements [section 5.4.3 of `draft-irtf-cfrg-hash-to-curve-12`][dst]. -/// -/// [dst]: https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-12#section-5.4.3 -#[derive(Debug)] -enum ExpandMsgDst<'x, L: ArrayLength> { - /// DST produced by hashing a very long (> 255 chars) input DST. - Hashed(GenericArray), - /// A raw input DST (<= 255 chars). - Raw(&'x [u8]), -} - -impl<'x, L: ArrayLength> ExpandMsgDst<'x, L> { - /// Produces a DST for use with `expand_message_xof`. - pub fn process_xof(dst: &'x [u8]) -> Self - where - H: Default + Update + ExtendableOutputDirty, - { - if dst.len() > 255 { - let mut data = GenericArray::::default(); - H::default() - .chain(OVERSIZE_DST_SALT) - .chain(&dst) - .finalize_xof_dirty() - .read(&mut data); - Self::Hashed(data) - } else { - Self::Raw(dst) - } - } - - /// Produces a DST for use with `expand_message_xmd`. - pub fn process_xmd(dst: &'x [u8]) -> Self - where - H: Digest, - { - if dst.len() > 255 { - Self::Hashed(H::new().chain(OVERSIZE_DST_SALT).chain(&dst).finalize()) - } else { - Self::Raw(dst) - } - } - - /// Returns the raw bytes of the DST. - pub fn data(&'x self) -> &'x [u8] { - match self { - Self::Hashed(arr) => &arr[..], - Self::Raw(buf) => buf, - } - } - - /// Returns the length of the DST. - pub fn len(&'x self) -> usize { - match self { - Self::Hashed(_) => L::to_usize(), - Self::Raw(buf) => buf.len(), - } - } -} - -/// A trait for message expansion methods supported by hash-to-curve. -pub trait ExpandMessage: for<'x> InitExpandMessage<'x> { - // This intermediate is likely only necessary until GATs allow - // associated types with lifetimes. -} - -/// Trait for constructing a new message expander. -pub trait InitExpandMessage<'x> { - /// The state object used during message expansion. - type Expander: ExpandMessageState<'x>; - - /// Initializes a message expander. - fn init_expand(message: &[u8], dst: &'x [u8], len_in_bytes: usize) -> Self::Expander; -} - -// Automatically derive trait -impl InitExpandMessage<'x>> ExpandMessage for X {} - -/// Trait for types implementing the `expand_message` interface for `hash_to_field`. -pub trait ExpandMessageState<'x> { - /// Reads bytes from the generated output. - fn read_into(&mut self, output: &mut [u8]) -> usize; - - /// Retrieves the number of bytes remaining in the generator. - fn remain(&self) -> usize; - - #[cfg(feature = "alloc")] - /// Constructs a `Vec` containing the remaining bytes of the output. - fn into_vec(mut self) -> Vec - where - Self: Sized, - { - let mut result = alloc::vec![0u8; self.remain()]; - self.read_into(&mut result[..]); - result - } -} - -/// A generator for the output of `expand_message_xof` for a given -/// extendable hash function, message, DST, and output length. -/// -/// Implements [section 5.4.2 of `draft-irtf-cfrg-hash-to-curve-12`][expand_message_xof] -/// with `k = 128`. -/// -/// [expand_message_xof]: https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-12#section-5.4.2 -pub struct ExpandMsgXof { - hash: ::Reader, - remain: usize, -} - -impl Debug for ExpandMsgXof { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - f.debug_struct("ExpandMsgXof") - .field("remain", &self.remain) - .finish() - } -} - -impl<'x, H> ExpandMessageState<'x> for ExpandMsgXof -where - H: ExtendableOutputDirty, -{ - fn read_into(&mut self, output: &mut [u8]) -> usize { - let len = self.remain.min(output.len()); - self.hash.read(&mut output[..len]); - self.remain -= len; - len - } - - fn remain(&self) -> usize { - self.remain - } -} - -impl<'x, H> InitExpandMessage<'x> for ExpandMsgXof -where - H: Default + Update + ExtendableOutputDirty, -{ - type Expander = Self; - - fn init_expand(message: &[u8], dst: &[u8], len_in_bytes: usize) -> Self { - // Use U32 here for k = 128. - let dst = ExpandMsgDst::::process_xof::(dst); - let hash = H::default() - .chain(message) - .chain((len_in_bytes as u16).to_be_bytes()) - .chain(dst.data()) - .chain([dst.len() as u8]) - .finalize_xof_dirty(); - Self { - hash, - remain: len_in_bytes, - } - } -} - -/// Constructor for `expand_message_xmd` for a given digest hash function, message, DST, -/// and output length. -/// -/// Implements [section 5.4.1 of `draft-irtf-cfrg-hash-to-curve-12`][expand_message_xmd]. -/// -/// [expand_message_xmd]: https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-12#section-5.4.1 -#[derive(Debug)] -pub struct ExpandMsgXmd(PhantomData); - -/// A generator for the output of `expand_message_xmd` for a given -/// digest hash function, message, DST, and output length. -/// -/// Implements [section 5.4.1 of `draft-irtf-cfrg-hash-to-curve-12`][expand_message_xmd]. -/// -/// [expand_message_xmd]: https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-12#section-5.4.1 -pub struct ExpandMsgXmdState<'x, H: Digest> { - dst: ExpandMsgDst<'x, H::OutputSize>, - b_0: GenericArray, - b_i: GenericArray, - i: usize, - b_offs: usize, - remain: usize, -} - -impl Debug for ExpandMsgXmdState<'_, H> { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - f.debug_struct("ExpandMsgXmdState") - .field("remain", &self.remain) - .finish() - } -} - -impl<'x, H> InitExpandMessage<'x> for ExpandMsgXmd -where - H: Digest + BlockInput, -{ - type Expander = ExpandMsgXmdState<'x, H>; - - fn init_expand(message: &[u8], dst: &'x [u8], len_in_bytes: usize) -> Self::Expander { - let hash_size = ::OutputSize::to_usize(); - let ell = (len_in_bytes + hash_size - 1) / hash_size; - if ell > 255 { - panic!("Invalid ExpandMsgXmd usage: ell > 255"); - } - let dst = ExpandMsgDst::process_xmd::(dst); - let b_0 = H::new() - .chain(GenericArray::::BlockSize>::default()) - .chain(message) - .chain((len_in_bytes as u16).to_be_bytes()) - .chain([0u8]) - .chain(dst.data()) - .chain([dst.len() as u8]) - .finalize(); - // init with b_1 - let b_i = H::new() - .chain(&b_0) - .chain([1u8]) - .chain(dst.data()) - .chain([dst.len() as u8]) - .finalize(); - ExpandMsgXmdState { - dst, - b_0, - b_i, - i: 2, - b_offs: 0, - remain: len_in_bytes, - } - } -} - -impl<'x, H> ExpandMessageState<'x> for ExpandMsgXmdState<'x, H> -where - H: Digest + BlockInput, -{ - fn read_into(&mut self, output: &mut [u8]) -> usize { - let read_len = self.remain.min(output.len()); - let mut offs = 0; - let hash_size = H::OutputSize::to_usize(); - while offs < read_len { - let b_offs = self.b_offs; - let mut copy_len = hash_size - b_offs; - if copy_len > 0 { - copy_len = copy_len.min(read_len - offs); - output[offs..(offs + copy_len)] - .copy_from_slice(&self.b_i[b_offs..(b_offs + copy_len)]); - offs += copy_len; - self.b_offs = b_offs + copy_len; - } else { - let mut b_prev_xor = self.b_0.clone(); - for j in 0..hash_size { - b_prev_xor[j] ^= self.b_i[j]; - } - self.b_i = H::new() - .chain(b_prev_xor) - .chain([self.i as u8]) - .chain(self.dst.data()) - .chain([self.dst.len() as u8]) - .finalize(); - self.b_offs = 0; - self.i += 1; - } - } - self.remain -= read_len; - read_len - } - - fn remain(&self) -> usize { - self.remain - } -} - -#[cfg(feature = "alloc")] -#[cfg(test)] -mod tests { - use super::*; - use sha2::{Sha256, Sha512}; - use sha3::{Shake128, Shake256}; - - /// From - #[test] - fn expand_message_xmd_works_for_draft12_testvectors_sha256() { - let dst = b"QUUX-V01-CS02-with-expander-SHA256-128"; - - let msg = b""; - let len_in_bytes = 0x20; - let uniform_bytes = hex::decode( - "68a985b87eb6b46952128911f2a4412bbc302a9d759667f8\ - 7f7a21d803f07235", - ) - .unwrap(); - assert_eq!( - ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b"abc"; - let len_in_bytes = 0x20; - let uniform_bytes = hex::decode( - "d8ccab23b5985ccea865c6c97b6e5b8350e794e603b4b979\ - 02f53a8a0d605615", - ) - .unwrap(); - assert_eq!( - ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b"abcdef0123456789"; - let len_in_bytes = 0x20; - let uniform_bytes = hex::decode( - "eff31487c770a893cfb36f912fbfcbff40d5661771ca4b2c\ - b4eafe524333f5c1", - ) - .unwrap(); - assert_eq!( - ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b"q128_qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ - qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ - qqqqqqqqqqqqqqqqqqqqqqqqq"; - let len_in_bytes = 0x20; - let uniform_bytes = hex::decode( - "b23a1d2b4d97b2ef7785562a7e8bac7eed54ed6e97e29aa5\ - 1bfe3f12ddad1ff9", - ) - .unwrap(); - assert_eq!( - ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b"a512_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - let len_in_bytes = 0x20; - let uniform_bytes = hex::decode( - "4623227bcc01293b8c130bf771da8c298dede7383243dc09\ - 93d2d94823958c4c", - ) - .unwrap(); - assert_eq!( - ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b""; - let len_in_bytes = 0x80; - let uniform_bytes = hex::decode( - "af84c27ccfd45d41914fdff5df25293e221afc53d8ad2ac0\ - 6d5e3e29485dadbee0d121587713a3e0dd4d5e69e93eb7cd4f5df4\ - cd103e188cf60cb02edc3edf18eda8576c412b18ffb658e3dd6ec8\ - 49469b979d444cf7b26911a08e63cf31f9dcc541708d3491184472\ - c2c29bb749d4286b004ceb5ee6b9a7fa5b646c993f0ced", - ) - .unwrap(); - assert_eq!( - ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b"abc"; - let len_in_bytes = 0x80; - let uniform_bytes = hex::decode( - "abba86a6129e366fc877aab32fc4ffc70120d8996c88aee2\ - fe4b32d6c7b6437a647e6c3163d40b76a73cf6a5674ef1d890f95b\ - 664ee0afa5359a5c4e07985635bbecbac65d747d3d2da7ec2b8221\ - b17b0ca9dc8a1ac1c07ea6a1e60583e2cb00058e77b7b72a298425\ - cd1b941ad4ec65e8afc50303a22c0f99b0509b4c895f40", - ) - .unwrap(); - assert_eq!( - ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b"abcdef0123456789"; - let len_in_bytes = 0x80; - let uniform_bytes = hex::decode( - "ef904a29bffc4cf9ee82832451c946ac3c8f8058ae97d8d6\ - 29831a74c6572bd9ebd0df635cd1f208e2038e760c4994984ce73f\ - 0d55ea9f22af83ba4734569d4bc95e18350f740c07eef653cbb9f8\ - 7910d833751825f0ebefa1abe5420bb52be14cf489b37fe1a72f7d\ - e2d10be453b2c9d9eb20c7e3f6edc5a60629178d9478df", - ) - .unwrap(); - assert_eq!( - ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b"q128_qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ - qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ - qqqqqqqqqqqqqqqqqqqqqqqqq"; - let len_in_bytes = 0x80; - let uniform_bytes = hex::decode( - "80be107d0884f0d881bb460322f0443d38bd222db8bd0b0a\ - 5312a6fedb49c1bbd88fd75d8b9a09486c60123dfa1d73c1cc3169\ - 761b17476d3c6b7cbbd727acd0e2c942f4dd96ae3da5de368d26b3\ - 2286e32de7e5a8cb2949f866a0b80c58116b29fa7fabb3ea7d520e\ - e603e0c25bcaf0b9a5e92ec6a1fe4e0391d1cdbce8c68a", - ) - .unwrap(); - assert_eq!( - ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b"a512_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - let len_in_bytes = 0x80; - let uniform_bytes = hex::decode( - "546aff5444b5b79aa6148bd81728704c32decb73a3ba76e9\ - e75885cad9def1d06d6792f8a7d12794e90efed817d96920d72889\ - 6a4510864370c207f99bd4a608ea121700ef01ed879745ee3e4cee\ - f777eda6d9e5e38b90c86ea6fb0b36504ba4a45d22e86f6db5dd43\ - d98a294bebb9125d5b794e9d2a81181066eb954966a487", - ) - .unwrap(); - assert_eq!( - ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - } - - /// From - #[test] - fn expand_message_xmd_works_for_draft12_testvectors_sha256_long_dst() { - let dst = b"QUUX-V01-CS02-with-expander-SHA256-128-long-DST-111111\ - 111111111111111111111111111111111111111111111111111111\ - 111111111111111111111111111111111111111111111111111111\ - 111111111111111111111111111111111111111111111111111111\ - 1111111111111111111111111111111111111111"; - - let msg = b""; - let len_in_bytes = 0x20; - let uniform_bytes = hex::decode( - "e8dc0c8b686b7ef2074086fbdd2f30e3f8bfbd3bdf177f73\ - f04b97ce618a3ed3", - ) - .unwrap(); - assert_eq!( - ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b"abc"; - let len_in_bytes = 0x20; - let uniform_bytes = hex::decode( - "52dbf4f36cf560fca57dedec2ad924ee9c266341d8f3d6af\ - e5171733b16bbb12", - ) - .unwrap(); - assert_eq!( - ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b"abcdef0123456789"; - let len_in_bytes = 0x20; - let uniform_bytes = hex::decode( - "35387dcf22618f3728e6c686490f8b431f76550b0b2c61cb\ - c1ce7001536f4521", - ) - .unwrap(); - assert_eq!( - ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b"q128_qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ - qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ - qqqqqqqqqqqqqqqqqqqqqqqqq"; - let len_in_bytes = 0x20; - let uniform_bytes = hex::decode( - "01b637612bb18e840028be900a833a74414140dde0c4754c\ - 198532c3a0ba42bc", - ) - .unwrap(); - assert_eq!( - ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b"a512_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - let len_in_bytes = 0x20; - let uniform_bytes = hex::decode( - "20cce7033cabc5460743180be6fa8aac5a103f56d481cf36\ - 9a8accc0c374431b", - ) - .unwrap(); - assert_eq!( - ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b""; - let len_in_bytes = 0x80; - let uniform_bytes = hex::decode( - "14604d85432c68b757e485c8894db3117992fc57e0e136f7\ - 1ad987f789a0abc287c47876978e2388a02af86b1e8d1342e5ce4f\ - 7aaa07a87321e691f6fba7e0072eecc1218aebb89fb14a0662322d\ - 5edbd873f0eb35260145cd4e64f748c5dfe60567e126604bcab1a3\ - ee2dc0778102ae8a5cfd1429ebc0fa6bf1a53c36f55dfc", - ) - .unwrap(); - assert_eq!( - ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b"abc"; - let len_in_bytes = 0x80; - let uniform_bytes = hex::decode( - "1a30a5e36fbdb87077552b9d18b9f0aee16e80181d5b951d\ - 0471d55b66684914aef87dbb3626eaabf5ded8cd0686567e503853\ - e5c84c259ba0efc37f71c839da2129fe81afdaec7fbdc0ccd4c794\ - 727a17c0d20ff0ea55e1389d6982d1241cb8d165762dbc39fb0cee\ - 4474d2cbbd468a835ae5b2f20e4f959f56ab24cd6fe267", - ) - .unwrap(); - assert_eq!( - ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b"abcdef0123456789"; - let len_in_bytes = 0x80; - let uniform_bytes = hex::decode( - "d2ecef3635d2397f34a9f86438d772db19ffe9924e28a1ca\ - f6f1c8f15603d4028f40891044e5c7e39ebb9b31339979ff33a424\ - 9206f67d4a1e7c765410bcd249ad78d407e303675918f20f26ce6d\ - 7027ed3774512ef5b00d816e51bfcc96c3539601fa48ef1c07e494\ - bdc37054ba96ecb9dbd666417e3de289d4f424f502a982", - ) - .unwrap(); - assert_eq!( - ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b"q128_qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ - qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ - qqqqqqqqqqqqqqqqqqqqqqqqq"; - let len_in_bytes = 0x80; - let uniform_bytes = hex::decode( - "ed6e8c036df90111410431431a232d41a32c86e296c05d42\ - 6e5f44e75b9a50d335b2412bc6c91e0a6dc131de09c43110d9180d\ - 0a70f0d6289cb4e43b05f7ee5e9b3f42a1fad0f31bac6a625b3b5c\ - 50e3a83316783b649e5ecc9d3b1d9471cb5024b7ccf40d41d1751a\ - 04ca0356548bc6e703fca02ab521b505e8e45600508d32", - ) - .unwrap(); - assert_eq!( - ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b"a512_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - let len_in_bytes = 0x80; - let uniform_bytes = hex::decode( - "78b53f2413f3c688f07732c10e5ced29a17c6a16f717179f\ - fbe38d92d6c9ec296502eb9889af83a1928cd162e845b0d3c5424e\ - 83280fed3d10cffb2f8431f14e7a23f4c68819d40617589e4c4116\ - 9d0b56e0e3535be1fd71fbb08bb70c5b5ffed953d6c14bf7618b35\ - fc1f4c4b30538236b4b08c9fbf90462447a8ada60be495", - ) - .unwrap(); - assert_eq!( - ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - } - - /// From - #[test] - fn expand_message_xmd_works_for_draft12_testvectors_sha512() { - let dst = b"QUUX-V01-CS02-with-expander-SHA512-256"; - - let msg = b""; - let len_in_bytes = 0x20; - let uniform_bytes = hex::decode( - "6b9a7312411d92f921c6f68ca0b6380730a1a4d982c50721\ - 1a90964c394179ba", - ) - .unwrap(); - assert_eq!( - ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b"abc"; - let len_in_bytes = 0x20; - let uniform_bytes = hex::decode( - "0da749f12fbe5483eb066a5f595055679b976e93abe9be6f\ - 0f6318bce7aca8dc", - ) - .unwrap(); - assert_eq!( - ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b"abcdef0123456789"; - let len_in_bytes = 0x20; - let uniform_bytes = hex::decode( - "087e45a86e2939ee8b91100af1583c4938e0f5fc6c9db4b1\ - 07b83346bc967f58", - ) - .unwrap(); - assert_eq!( - ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b"q128_qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ - qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ - qqqqqqqqqqqqqqqqqqqqqqqqq"; - let len_in_bytes = 0x20; - let uniform_bytes = hex::decode( - "7336234ee9983902440f6bc35b348352013becd88938d2af\ - ec44311caf8356b3", - ) - .unwrap(); - assert_eq!( - ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b"a512_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - let len_in_bytes = 0x20; - let uniform_bytes = hex::decode( - "57b5f7e766d5be68a6bfe1768e3c2b7f1228b3e4b3134956\ - dd73a59b954c66f4", - ) - .unwrap(); - assert_eq!( - ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b""; - let len_in_bytes = 0x80; - let uniform_bytes = hex::decode( - "41b037d1734a5f8df225dd8c7de38f851efdb45c372887be\ - 655212d07251b921b052b62eaed99b46f72f2ef4cc96bfaf254ebb\ - bec091e1a3b9e4fb5e5b619d2e0c5414800a1d882b62bb5cd1778f\ - 098b8eb6cb399d5d9d18f5d5842cf5d13d7eb00a7cff859b605da6\ - 78b318bd0e65ebff70bec88c753b159a805d2c89c55961", - ) - .unwrap(); - assert_eq!( - ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b"abc"; - let len_in_bytes = 0x80; - let uniform_bytes = hex::decode( - "7f1dddd13c08b543f2e2037b14cefb255b44c83cc397c178\ - 6d975653e36a6b11bdd7732d8b38adb4a0edc26a0cef4bb4521713\ - 5456e58fbca1703cd6032cb1347ee720b87972d63fbf232587043e\ - d2901bce7f22610c0419751c065922b488431851041310ad659e4b\ - 23520e1772ab29dcdeb2002222a363f0c2b1c972b3efe1", - ) - .unwrap(); - assert_eq!( - ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b"abcdef0123456789"; - let len_in_bytes = 0x80; - let uniform_bytes = hex::decode( - "3f721f208e6199fe903545abc26c837ce59ac6fa45733f1b\ - aaf0222f8b7acb0424814fcb5eecf6c1d38f06e9d0a6ccfbf85ae6\ - 12ab8735dfdf9ce84c372a77c8f9e1c1e952c3a61b7567dd069301\ - 6af51d2745822663d0c2367e3f4f0bed827feecc2aaf98c949b5ed\ - 0d35c3f1023d64ad1407924288d366ea159f46287e61ac", - ) - .unwrap(); - assert_eq!( - ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b"q128_qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ - qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ - qqqqqqqqqqqqqqqqqqqqqqqqq"; - let len_in_bytes = 0x80; - let uniform_bytes = hex::decode( - "b799b045a58c8d2b4334cf54b78260b45eec544f9f2fb5bd\ - 12fb603eaee70db7317bf807c406e26373922b7b8920fa29142703\ - dd52bdf280084fb7ef69da78afdf80b3586395b433dc66cde048a2\ - 58e476a561e9deba7060af40adf30c64249ca7ddea79806ee5beb9\ - a1422949471d267b21bc88e688e4014087a0b592b695ed", - ) - .unwrap(); - assert_eq!( - ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b"a512_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - let len_in_bytes = 0x80; - let uniform_bytes = hex::decode( - "05b0bfef265dcee87654372777b7c44177e2ae4c13a27f10\ - 3340d9cd11c86cb2426ffcad5bd964080c2aee97f03be1ca18e30a\ - 1f14e27bc11ebbd650f305269cc9fb1db08bf90bfc79b42a952b46\ - daf810359e7bc36452684784a64952c343c52e5124cd1f71d474d5\ - 197fefc571a92929c9084ffe1112cf5eea5192ebff330b", - ) - .unwrap(); - assert_eq!( - ExpandMsgXmd::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - } - - /// From - #[test] - fn expand_message_xof_works_for_draft12_testvectors_shake128() { - let dst = b"QUUX-V01-CS02-with-expander-SHAKE128"; - - let msg = b""; - let len_in_bytes = 0x20; - let uniform_bytes = hex::decode( - "86518c9cd86581486e9485aa74ab35ba150d1c75c88e26b7\ - 043e44e2acd735a2", - ) - .unwrap(); - assert_eq!( - ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b"abc"; - let len_in_bytes = 0x20; - let uniform_bytes = hex::decode( - "8696af52a4d862417c0763556073f47bc9b9ba43c99b5053\ - 05cb1ec04a9ab468", - ) - .unwrap(); - assert_eq!( - ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b"abcdef0123456789"; - let len_in_bytes = 0x20; - let uniform_bytes = hex::decode( - "912c58deac4821c3509dbefa094df54b34b8f5d01a191d1d\ - 3108a2c89077acca", - ) - .unwrap(); - assert_eq!( - ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b"q128_qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ - qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ - qqqqqqqqqqqqqqqqqqqqqqqqq"; - let len_in_bytes = 0x20; - let uniform_bytes = hex::decode( - "1adbcc448aef2a0cebc71dac9f756b22e51839d348e031e6\ - 3b33ebb50faeaf3f", - ) - .unwrap(); - assert_eq!( - ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b"a512_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - let len_in_bytes = 0x20; - let uniform_bytes = hex::decode( - "df3447cc5f3e9a77da10f819218ddf31342c310778e0e4ef\ - 72bbaecee786a4fe", - ) - .unwrap(); - assert_eq!( - ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b""; - let len_in_bytes = 0x80; - let uniform_bytes = hex::decode( - "7314ff1a155a2fb99a0171dc71b89ab6e3b2b7d59e38e644\ - 19b8b6294d03ffee42491f11370261f436220ef787f8f76f5b26bd\ - cd850071920ce023f3ac46847744f4612b8714db8f5db83205b2e6\ - 25d95afd7d7b4d3094d3bdde815f52850bb41ead9822e08f22cf41\ - d615a303b0d9dde73263c049a7b9898208003a739a2e57", - ) - .unwrap(); - assert_eq!( - ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b"abc"; - let len_in_bytes = 0x80; - let uniform_bytes = hex::decode( - "c952f0c8e529ca8824acc6a4cab0e782fc3648c563ddb00d\ - a7399f2ae35654f4860ec671db2356ba7baa55a34a9d7f79197b60\ - ddae6e64768a37d699a78323496db3878c8d64d909d0f8a7de4927\ - dcab0d3dbbc26cb20a49eceb0530b431cdf47bc8c0fa3e0d88f53b\ - 318b6739fbed7d7634974f1b5c386d6230c76260d5337a", - ) - .unwrap(); - assert_eq!( - ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b"abcdef0123456789"; - let len_in_bytes = 0x80; - let uniform_bytes = hex::decode( - "19b65ee7afec6ac06a144f2d6134f08eeec185f1a890fe34\ - e68f0e377b7d0312883c048d9b8a1d6ecc3b541cb4987c26f45e0c\ - 82691ea299b5e6889bbfe589153016d8131717ba26f07c3c14ffbe\ - f1f3eff9752e5b6183f43871a78219a75e7000fbac6a7072e2b83c\ - 790a3a5aecd9d14be79f9fd4fb180960a3772e08680495", - ) - .unwrap(); - assert_eq!( - ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b"q128_qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ - qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ - qqqqqqqqqqqqqqqqqqqqqqqqq"; - let len_in_bytes = 0x80; - let uniform_bytes = hex::decode( - "ca1b56861482b16eae0f4a26212112362fcc2d76dcc80c93\ - c4182ed66c5113fe41733ed68be2942a3487394317f3379856f482\ - 2a611735e50528a60e7ade8ec8c71670fec6661e2c59a09ed36386\ - 513221688b35dc47e3c3111ee8c67ff49579089d661caa29db1ef1\ - 0eb6eace575bf3dc9806e7c4016bd50f3c0e2a6481ee6d", - ) - .unwrap(); - assert_eq!( - ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b"a512_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - let len_in_bytes = 0x80; - let uniform_bytes = hex::decode( - "9d763a5ce58f65c91531b4100c7266d479a5d9777ba76169\ - 3d052acd37d149e7ac91c796a10b919cd74a591a1e38719fb91b72\ - 03e2af31eac3bff7ead2c195af7d88b8bc0a8adf3d1e90ab9bed6d\ - dc2b7f655dd86c730bdeaea884e73741097142c92f0e3fc1811b69\ - 9ba593c7fbd81da288a29d423df831652e3a01a9374999", - ) - .unwrap(); - assert_eq!( - ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - } - - /// From - #[test] - fn expand_message_xof_works_for_draft12_testvectors_shake128_long_dst() { - let dst = b"QUUX-V01-CS02-with-expander-SHAKE128-long-DST-11111111\ - 111111111111111111111111111111111111111111111111111111\ - 111111111111111111111111111111111111111111111111111111\ - 111111111111111111111111111111111111111111111111111111\ - 1111111111111111111111111111111111111111"; - - let msg = b""; - let len_in_bytes = 0x20; - let uniform_bytes = hex::decode( - "827c6216330a122352312bccc0c8d6e7a146c5257a776dbd\ - 9ad9d75cd880fc53", - ) - .unwrap(); - assert_eq!( - ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b"abc"; - let len_in_bytes = 0x20; - let uniform_bytes = hex::decode( - "690c8d82c7213b4282c6cb41c00e31ea1d3e2005f93ad19b\ - bf6da40f15790c5c", - ) - .unwrap(); - assert_eq!( - ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b"abcdef0123456789"; - let len_in_bytes = 0x20; - let uniform_bytes = hex::decode( - "979e3a15064afbbcf99f62cc09fa9c85028afcf3f825eb07\ - 11894dcfc2f57057", - ) - .unwrap(); - assert_eq!( - ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b"q128_qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ - qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ - qqqqqqqqqqqqqqqqqqqqqqqqq"; - let len_in_bytes = 0x20; - let uniform_bytes = hex::decode( - "c5a9220962d9edc212c063f4f65b609755a1ed96e62f9db5\ - d1fd6adb5a8dc52b", - ) - .unwrap(); - assert_eq!( - ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b"a512_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - let len_in_bytes = 0x20; - let uniform_bytes = hex::decode( - "f7b96a5901af5d78ce1d071d9c383cac66a1dfadb508300e\ - c6aeaea0d62d5d62", - ) - .unwrap(); - assert_eq!( - ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b""; - let len_in_bytes = 0x80; - let uniform_bytes = hex::decode( - "3890dbab00a2830be398524b71c2713bbef5f4884ac2e6f0\ - 70b092effdb19208c7df943dc5dcbaee3094a78c267ef276632ee2\ - c8ea0c05363c94b6348500fae4208345dd3475fe0c834c2beac7fa\ - 7bc181692fb728c0a53d809fc8111495222ce0f38468b11becb15b\ - 32060218e285c57a60162c2c8bb5b6bded13973cd41819", - ) - .unwrap(); - assert_eq!( - ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b"abc"; - let len_in_bytes = 0x80; - let uniform_bytes = hex::decode( - "41b7ffa7a301b5c1441495ebb9774e2a53dbbf4e54b9a1af\ - 6a20fd41eafd69ef7b9418599c5545b1ee422f363642b01d4a5344\ - 9313f68da3e49dddb9cd25b97465170537d45dcbdf92391b5bdff3\ - 44db4bd06311a05bca7dcd360b6caec849c299133e5c9194f4e15e\ - 3e23cfaab4003fab776f6ac0bfae9144c6e2e1c62e7d57", - ) - .unwrap(); - assert_eq!( - ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b"abcdef0123456789"; - let len_in_bytes = 0x80; - let uniform_bytes = hex::decode( - "55317e4a21318472cd2290c3082957e1242241d9e0d04f47\ - 026f03401643131401071f01aa03038b2783e795bdfa8a3541c194\ - ad5de7cb9c225133e24af6c86e748deb52e560569bd54ef4dac034\ - 65111a3a44b0ea490fb36777ff8ea9f1a8a3e8e0de3cf0880b4b2f\ - 8dd37d3a85a8b82375aee4fa0e909f9763319b55778e71", - ) - .unwrap(); - assert_eq!( - ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b"q128_qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ - qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ - qqqqqqqqqqqqqqqqqqqqqqqqq"; - let len_in_bytes = 0x80; - let uniform_bytes = hex::decode( - "19fdd2639f082e31c77717ac9bb032a22ff0958382b2dbb3\ - 9020cdc78f0da43305414806abf9a561cb2d0067eb2f7bc544482f\ - 75623438ed4b4e39dd9e6e2909dd858bd8f1d57cd0fce2d3150d90\ - aa67b4498bdf2df98c0100dd1a173436ba5d0df6be1defb0b2ce55\ - ccd2f4fc05eb7cb2c019c35d5398b85adc676da4238bc7", - ) - .unwrap(); - assert_eq!( - ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b"a512_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - let len_in_bytes = 0x80; - let uniform_bytes = hex::decode( - "945373f0b3431a103333ba6a0a34f1efab2702efde41754c\ - 4cb1d5216d5b0a92a67458d968562bde7fa6310a83f53dda138368\ - 0a276a283438d58ceebfa7ab7ba72499d4a3eddc860595f63c93b1\ - c5e823ea41fc490d938398a26db28f61857698553e93f0574eb8c5\ - 017bfed6249491f9976aaa8d23d9485339cc85ca329308", - ) - .unwrap(); - assert_eq!( - ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - } - - /// From - #[test] - fn expand_message_xof_works_for_draft12_testvectors_shake256() { - let dst = b"QUUX-V01-CS02-with-expander-SHAKE256"; - - let msg = b""; - let len_in_bytes = 0x20; - let uniform_bytes = hex::decode( - "2ffc05c48ed32b95d72e807f6eab9f7530dd1c2f013914c8\ - fed38c5ccc15ad76", - ) - .unwrap(); - assert_eq!( - ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b"abc"; - let len_in_bytes = 0x20; - let uniform_bytes = hex::decode( - "b39e493867e2767216792abce1f2676c197c0692aed06156\ - 0ead251821808e07", - ) - .unwrap(); - assert_eq!( - ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b"abcdef0123456789"; - let len_in_bytes = 0x20; - let uniform_bytes = hex::decode( - "245389cf44a13f0e70af8665fe5337ec2dcd138890bb7901\ - c4ad9cfceb054b65", - ) - .unwrap(); - assert_eq!( - ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b"q128_qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ - qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ - qqqqqqqqqqqqqqqqqqqqqqqqq"; - let len_in_bytes = 0x20; - let uniform_bytes = hex::decode( - "719b3911821e6428a5ed9b8e600f2866bcf23c8f0515e52d\ - 6c6c019a03f16f0e", - ) - .unwrap(); - assert_eq!( - ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b"a512_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - let len_in_bytes = 0x20; - let uniform_bytes = hex::decode( - "9181ead5220b1963f1b5951f35547a5ea86a820562287d6c\ - a4723633d17ccbbc", - ) - .unwrap(); - assert_eq!( - ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b""; - let len_in_bytes = 0x80; - let uniform_bytes = hex::decode( - "7a1361d2d7d82d79e035b8880c5a3c86c5afa719478c007d\ - 96e6c88737a3f631dd74a2c88df79a4cb5e5d9f7504957c70d669e\ - c6bfedc31e01e2bacc4ff3fdf9b6a00b17cc18d9d72ace7d6b81c2\ - e481b4f73f34f9a7505dccbe8f5485f3d20c5409b0310093d5d649\ - 2dea4e18aa6979c23c8ea5de01582e9689612afbb353df", - ) - .unwrap(); - assert_eq!( - ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b"abc"; - let len_in_bytes = 0x80; - let uniform_bytes = hex::decode( - "a54303e6b172909783353ab05ef08dd435a558c3197db0c1\ - 32134649708e0b9b4e34fb99b92a9e9e28fc1f1d8860d85897a8e0\ - 21e6382f3eea10577f968ff6df6c45fe624ce65ca25932f679a42a\ - 404bc3681efe03fcd45ef73bb3a8f79ba784f80f55ea8a3c367408\ - f30381299617f50c8cf8fbb21d0f1e1d70b0131a7b6fbe", - ) - .unwrap(); - assert_eq!( - ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b"abcdef0123456789"; - let len_in_bytes = 0x80; - let uniform_bytes = hex::decode( - "e42e4d9538a189316e3154b821c1bafb390f78b2f010ea40\ - 4e6ac063deb8c0852fcd412e098e231e43427bd2be1330bb47b403\ - 9ad57b30ae1fc94e34993b162ff4d695e42d59d9777ea18d3848d9\ - d336c25d2acb93adcad009bcfb9cde12286df267ada283063de0bb\ - 1505565b2eb6c90e31c48798ecdc71a71756a9110ff373", - ) - .unwrap(); - assert_eq!( - ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b"q128_qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ - qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ - qqqqqqqqqqqqqqqqqqqqqqqqq"; - let len_in_bytes = 0x80; - let uniform_bytes = hex::decode( - "4ac054dda0a38a65d0ecf7afd3c2812300027c8789655e47\ - aecf1ecc1a2426b17444c7482c99e5907afd9c25b991990490bb9c\ - 686f43e79b4471a23a703d4b02f23c669737a886a7ec28bddb92c3\ - a98de63ebf878aa363a501a60055c048bea11840c4717beae7eee2\ - 8c3cfa42857b3d130188571943a7bd747de831bd6444e0", - ) - .unwrap(); - assert_eq!( - ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - - let msg = b"a512_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - let len_in_bytes = 0x80; - let uniform_bytes = hex::decode( - "09afc76d51c2cccbc129c2315df66c2be7295a231203b8ab\ - 2dd7f95c2772c68e500bc72e20c602abc9964663b7a03a389be128\ - c56971ce81001a0b875e7fd17822db9d69792ddf6a23a151bf4700\ - 79c518279aef3e75611f8f828994a9988f4a8a256ddb8bae161e65\ - 8d5a2a09bcfe839c6396dc06ee5c8ff3c22d3b1f9deb7e", - ) - .unwrap(); - assert_eq!( - ExpandMsgXof::::init_expand(msg, dst, len_in_bytes).into_vec(), - uniform_bytes - ); - } -} diff --git a/constantine/bls12_381/src/hash_to_curve/map_g1.rs b/constantine/bls12_381/src/hash_to_curve/map_g1.rs deleted file mode 100644 index 0f2c94e6e..000000000 --- a/constantine/bls12_381/src/hash_to_curve/map_g1.rs +++ /dev/null @@ -1,961 +0,0 @@ -//! Implementation of hash-to-curve for the G1 group. - -use subtle::{Choice, ConditionallyNegatable, ConditionallySelectable, ConstantTimeEq}; - -use super::chain::chain_pm3div4; -use super::{HashToField, MapToCurve, Sgn0}; -use crate::fp::Fp; -use crate::g1::G1Projective; -use crate::generic_array::{typenum::U64, GenericArray}; - -/// Coefficients of the 11-isogeny x map's numerator -const ISO11_XNUM: [Fp; 12] = [ - Fp::from_raw_unchecked([ - 0x4d18_b6f3_af00_131c, - 0x19fa_2197_93fe_e28c, - 0x3f28_85f1_467f_19ae, - 0x23dc_ea34_f2ff_b304, - 0xd15b_58d2_ffc0_0054, - 0x0913_be20_0a20_bef4, - ]), - Fp::from_raw_unchecked([ - 0x8989_8538_5cdb_bd8b, - 0x3c79_e43c_c7d9_66aa, - 0x1597_e193_f4cd_233a, - 0x8637_ef1e_4d66_23ad, - 0x11b2_2dee_d20d_827b, - 0x0709_7bc5_9987_84ad, - ]), - Fp::from_raw_unchecked([ - 0xa542_583a_480b_664b, - 0xfc71_69c0_26e5_68c6, - 0x5ba2_ef31_4ed8_b5a6, - 0x5b54_91c0_5102_f0e7, - 0xdf6e_9970_7d2a_0079, - 0x0784_151e_d760_5524, - ]), - Fp::from_raw_unchecked([ - 0x494e_2128_70f7_2741, - 0xab9b_e52f_bda4_3021, - 0x26f5_5779_94e3_4c3d, - 0x049d_fee8_2aef_bd60, - 0x65da_dd78_2850_5289, - 0x0e93_d431_ea01_1aeb, - ]), - Fp::from_raw_unchecked([ - 0x90ee_774b_d6a7_4d45, - 0x7ada_1c8a_41bf_b185, - 0x0f1a_8953_b325_f464, - 0x104c_2421_1be4_805c, - 0x1691_39d3_19ea_7a8f, - 0x09f2_0ead_8e53_2bf6, - ]), - Fp::from_raw_unchecked([ - 0x6ddd_93e2_f436_26b7, - 0xa548_2c9a_a1cc_d7bd, - 0x1432_4563_1883_f4bd, - 0x2e0a_94cc_f77e_c0db, - 0xb028_2d48_0e56_489f, - 0x18f4_bfcb_b436_8929, - ]), - Fp::from_raw_unchecked([ - 0x23c5_f0c9_5340_2dfd, - 0x7a43_ff69_58ce_4fe9, - 0x2c39_0d3d_2da5_df63, - 0xd0df_5c98_e1f9_d70f, - 0xffd8_9869_a572_b297, - 0x1277_ffc7_2f25_e8fe, - ]), - Fp::from_raw_unchecked([ - 0x79f4_f049_0f06_a8a6, - 0x85f8_94a8_8030_fd81, - 0x12da_3054_b18b_6410, - 0xe2a5_7f65_0588_0d65, - 0xbba0_74f2_60e4_00f1, - 0x08b7_6279_f621_d028, - ]), - Fp::from_raw_unchecked([ - 0xe672_45ba_78d5_b00b, - 0x8456_ba9a_1f18_6475, - 0x7888_bff6_e6b3_3bb4, - 0xe215_85b9_a30f_86cb, - 0x05a6_9cdc_ef55_feee, - 0x09e6_99dd_9adf_a5ac, - ]), - Fp::from_raw_unchecked([ - 0x0de5_c357_bff5_7107, - 0x0a0d_b4ae_6b1a_10b2, - 0xe256_bb67_b3b3_cd8d, - 0x8ad4_5657_4e9d_b24f, - 0x0443_915f_50fd_4179, - 0x098c_4bf7_de8b_6375, - ]), - Fp::from_raw_unchecked([ - 0xe6b0_617e_7dd9_29c7, - 0xfe6e_37d4_4253_7375, - 0x1daf_deda_137a_489e, - 0xe4ef_d1ad_3f76_7ceb, - 0x4a51_d866_7f0f_e1cf, - 0x054f_df4b_bf1d_821c, - ]), - Fp::from_raw_unchecked([ - 0x72db_2a50_658d_767b, - 0x8abf_91fa_a257_b3d5, - 0xe969_d683_3764_ab47, - 0x4641_7014_2a10_09eb, - 0xb14f_01aa_db30_be2f, - 0x18ae_6a85_6f40_715d, - ]), -]; - -/// Coefficients of the 11-isogeny x map's denominator -const ISO11_XDEN: [Fp; 11] = [ - Fp::from_raw_unchecked([ - 0xb962_a077_fdb0_f945, - 0xa6a9_740f_efda_13a0, - 0xc14d_568c_3ed6_c544, - 0xb43f_c37b_908b_133e, - 0x9c0b_3ac9_2959_9016, - 0x0165_aa6c_93ad_115f, - ]), - Fp::from_raw_unchecked([ - 0x2327_9a3b_a506_c1d9, - 0x92cf_ca0a_9465_176a, - 0x3b29_4ab1_3755_f0ff, - 0x116d_da1c_5070_ae93, - 0xed45_3092_4cec_2045, - 0x0833_83d6_ed81_f1ce, - ]), - Fp::from_raw_unchecked([ - 0x9885_c2a6_449f_ecfc, - 0x4a2b_54cc_d377_33f0, - 0x17da_9ffd_8738_c142, - 0xa0fb_a727_32b3_fafd, - 0xff36_4f36_e54b_6812, - 0x0f29_c13c_6605_23e2, - ]), - Fp::from_raw_unchecked([ - 0xe349_cc11_8278_f041, - 0xd487_228f_2f32_04fb, - 0xc9d3_2584_9ade_5150, - 0x43a9_2bd6_9c15_c2df, - 0x1c2c_7844_bc41_7be4, - 0x1202_5184_f407_440c, - ]), - Fp::from_raw_unchecked([ - 0x587f_65ae_6acb_057b, - 0x1444_ef32_5140_201f, - 0xfbf9_95e7_1270_da49, - 0xccda_0660_7243_6a42, - 0x7408_904f_0f18_6bb2, - 0x13b9_3c63_edf6_c015, - ]), - Fp::from_raw_unchecked([ - 0xfb91_8622_cd14_1920, - 0x4a4c_6442_3eca_ddb4, - 0x0beb_2329_27f7_fb26, - 0x30f9_4df6_f83a_3dc2, - 0xaeed_d424_d780_f388, - 0x06cc_402d_d594_bbeb, - ]), - Fp::from_raw_unchecked([ - 0xd41f_7611_51b2_3f8f, - 0x32a9_2465_4357_19b3, - 0x64f4_36e8_88c6_2cb9, - 0xdf70_a9a1_f757_c6e4, - 0x6933_a38d_5b59_4c81, - 0x0c6f_7f72_37b4_6606, - ]), - Fp::from_raw_unchecked([ - 0x693c_0874_7876_c8f7, - 0x22c9_850b_f9cf_80f0, - 0x8e90_71da_b950_c124, - 0x89bc_62d6_1c7b_af23, - 0xbc6b_e2d8_dad5_7c23, - 0x1791_6987_aa14_a122, - ]), - Fp::from_raw_unchecked([ - 0x1be3_ff43_9c13_16fd, - 0x9965_243a_7571_dfa7, - 0xc7f7_f629_62f5_cd81, - 0x32c6_aa9a_f394_361c, - 0xbbc2_ee18_e1c2_27f4, - 0x0c10_2cba_c531_bb34, - ]), - Fp::from_raw_unchecked([ - 0x9976_14c9_7bac_bf07, - 0x61f8_6372_b991_92c0, - 0x5b8c_95fc_1435_3fc3, - 0xca2b_066c_2a87_492f, - 0x1617_8f5b_bf69_8711, - 0x12a6_dcd7_f0f4_e0e8, - ]), - Fp::from_raw_unchecked([ - 0x7609_0000_0002_fffd, - 0xebf4_000b_c40c_0002, - 0x5f48_9857_53c7_58ba, - 0x77ce_5853_7052_5745, - 0x5c07_1a97_a256_ec6d, - 0x15f6_5ec3_fa80_e493, - ]), -]; - -/// Coefficients of the 11-isogeny y map's numerator -const ISO11_YNUM: [Fp; 16] = [ - Fp::from_raw_unchecked([ - 0x2b56_7ff3_e283_7267, - 0x1d4d_9e57_b958_a767, - 0xce02_8fea_04bd_7373, - 0xcc31_a30a_0b6c_d3df, - 0x7d7b_18a6_8269_2693, - 0x0d30_0744_d42a_0310, - ]), - Fp::from_raw_unchecked([ - 0x99c2_555f_a542_493f, - 0xfe7f_53cc_4874_f878, - 0x5df0_608b_8f97_608a, - 0x14e0_3832_052b_49c8, - 0x7063_26a6_957d_d5a4, - 0x0a8d_add9_c241_4555, - ]), - Fp::from_raw_unchecked([ - 0x13d9_4292_2a5c_f63a, - 0x357e_33e3_6e26_1e7d, - 0xcf05_a27c_8456_088d, - 0x0000_bd1d_e7ba_50f0, - 0x83d0_c753_2f8c_1fde, - 0x13f7_0bf3_8bbf_2905, - ]), - Fp::from_raw_unchecked([ - 0x5c57_fd95_bfaf_bdbb, - 0x28a3_59a6_5e54_1707, - 0x3983_ceb4_f636_0b6d, - 0xafe1_9ff6_f97e_6d53, - 0xb346_8f45_5019_2bf7, - 0x0bb6_cde4_9d8b_a257, - ]), - Fp::from_raw_unchecked([ - 0x590b_62c7_ff8a_513f, - 0x314b_4ce3_72ca_cefd, - 0x6bef_32ce_94b8_a800, - 0x6ddf_84a0_9571_3d5f, - 0x64ea_ce4c_b098_2191, - 0x0386_213c_651b_888d, - ]), - Fp::from_raw_unchecked([ - 0xa531_0a31_111b_bcdd, - 0xa14a_c0f5_da14_8982, - 0xf9ad_9cc9_5423_d2e9, - 0xaa6e_c095_283e_e4a7, - 0xcf5b_1f02_2e1c_9107, - 0x01fd_df5a_ed88_1793, - ]), - Fp::from_raw_unchecked([ - 0x65a5_72b0_d7a7_d950, - 0xe25c_2d81_8347_3a19, - 0xc2fc_ebe7_cb87_7dbd, - 0x05b2_d36c_769a_89b0, - 0xba12_961b_e86e_9efb, - 0x07eb_1b29_c1df_de1f, - ]), - Fp::from_raw_unchecked([ - 0x93e0_9572_f7c4_cd24, - 0x364e_9290_7679_5091, - 0x8569_467e_68af_51b5, - 0xa47d_a894_39f5_340f, - 0xf4fa_9180_82e4_4d64, - 0x0ad5_2ba3_e669_5a79, - ]), - Fp::from_raw_unchecked([ - 0x9114_2984_4e0d_5f54, - 0xd03f_51a3_516b_b233, - 0x3d58_7e56_4053_6e66, - 0xfa86_d2a3_a9a7_3482, - 0xa90e_d5ad_f1ed_5537, - 0x149c_9c32_6a5e_7393, - ]), - Fp::from_raw_unchecked([ - 0x462b_beb0_3c12_921a, - 0xdc9a_f5fa_0a27_4a17, - 0x9a55_8ebd_e836_ebed, - 0x649e_f8f1_1a4f_ae46, - 0x8100_e165_2b3c_dc62, - 0x1862_bd62_c291_dacb, - ]), - Fp::from_raw_unchecked([ - 0x05c9_b8ca_89f1_2c26, - 0x0194_160f_a9b9_ac4f, - 0x6a64_3d5a_6879_fa2c, - 0x1466_5bdd_8846_e19d, - 0xbb1d_0d53_af3f_f6bf, - 0x12c7_e1c3_b289_62e5, - ]), - Fp::from_raw_unchecked([ - 0xb55e_bf90_0b8a_3e17, - 0xfedc_77ec_1a92_01c4, - 0x1f07_db10_ea1a_4df4, - 0x0dfb_d15d_c41a_594d, - 0x3895_47f2_334a_5391, - 0x0241_9f98_1658_71a4, - ]), - Fp::from_raw_unchecked([ - 0xb416_af00_0745_fc20, - 0x8e56_3e9d_1ea6_d0f5, - 0x7c76_3e17_763a_0652, - 0x0145_8ef0_159e_bbef, - 0x8346_fe42_1f96_bb13, - 0x0d2d_7b82_9ce3_24d2, - ]), - Fp::from_raw_unchecked([ - 0x9309_6bb5_38d6_4615, - 0x6f2a_2619_951d_823a, - 0x8f66_b3ea_5951_4fa4, - 0xf563_e637_04f7_092f, - 0x724b_136c_4cf2_d9fa, - 0x0469_59cf_cfd0_bf49, - ]), - Fp::from_raw_unchecked([ - 0xea74_8d4b_6e40_5346, - 0x91e9_079c_2c02_d58f, - 0x4106_4965_946d_9b59, - 0xa067_31f1_d2bb_e1ee, - 0x07f8_97e2_67a3_3f1b, - 0x1017_2909_1921_0e5f, - ]), - Fp::from_raw_unchecked([ - 0x872a_a6c1_7d98_5097, - 0xeecc_5316_1264_562a, - 0x07af_e37a_fff5_5002, - 0x5475_9078_e5be_6838, - 0xc4b9_2d15_db8a_cca8, - 0x106d_87d1_b51d_13b9, - ]), -]; - -/// Coefficients of the 11-isogeny y map's denominator -const ISO11_YDEN: [Fp; 16] = [ - Fp::from_raw_unchecked([ - 0xeb6c_359d_47e5_2b1c, - 0x18ef_5f8a_1063_4d60, - 0xddfa_71a0_889d_5b7e, - 0x723e_71dc_c5fc_1323, - 0x52f4_5700_b70d_5c69, - 0x0a8b_981e_e476_91f1, - ]), - Fp::from_raw_unchecked([ - 0x616a_3c4f_5535_b9fb, - 0x6f5f_0373_95db_d911, - 0xf25f_4cc5_e35c_65da, - 0x3e50_dffe_a3c6_2658, - 0x6a33_dca5_2356_0776, - 0x0fad_eff7_7b6b_fe3e, - ]), - Fp::from_raw_unchecked([ - 0x2be9_b66d_f470_059c, - 0x24a2_c159_a3d3_6742, - 0x115d_be7a_d10c_2a37, - 0xb663_4a65_2ee5_884d, - 0x04fe_8bb2_b8d8_1af4, - 0x01c2_a7a2_56fe_9c41, - ]), - Fp::from_raw_unchecked([ - 0xf27b_f8ef_3b75_a386, - 0x898b_3674_76c9_073f, - 0x2448_2e6b_8c2f_4e5f, - 0xc8e0_bbd6_fe11_0806, - 0x59b0_c17f_7631_448a, - 0x1103_7cd5_8b3d_bfbd, - ]), - Fp::from_raw_unchecked([ - 0x31c7_912e_a267_eec6, - 0x1dbf_6f1c_5fcd_b700, - 0xd30d_4fe3_ba86_fdb1, - 0x3cae_528f_bee9_a2a4, - 0xb1cc_e69b_6aa9_ad9a, - 0x0443_93bb_632d_94fb, - ]), - Fp::from_raw_unchecked([ - 0xc66e_f6ef_eeb5_c7e8, - 0x9824_c289_dd72_bb55, - 0x71b1_a4d2_f119_981d, - 0x104f_c1aa_fb09_19cc, - 0x0e49_df01_d942_a628, - 0x096c_3a09_7732_72d4, - ]), - Fp::from_raw_unchecked([ - 0x9abc_11eb_5fad_eff4, - 0x32dc_a50a_8857_28f0, - 0xfb1f_a372_1569_734c, - 0xc4b7_6271_ea65_06b3, - 0xd466_a755_99ce_728e, - 0x0c81_d464_5f4c_b6ed, - ]), - Fp::from_raw_unchecked([ - 0x4199_f10e_5b8b_e45b, - 0xda64_e495_b1e8_7930, - 0xcb35_3efe_9b33_e4ff, - 0x9e9e_fb24_aa64_24c6, - 0xf08d_3368_0a23_7465, - 0x0d33_7802_3e4c_7406, - ]), - Fp::from_raw_unchecked([ - 0x7eb4_ae92_ec74_d3a5, - 0xc341_b4aa_9fac_3497, - 0x5be6_0389_9e90_7687, - 0x03bf_d9cc_a75c_bdeb, - 0x564c_2935_a96b_fa93, - 0x0ef3_c333_71e2_fdb5, - ]), - Fp::from_raw_unchecked([ - 0x7ee9_1fd4_49f6_ac2e, - 0xe5d5_bd5c_b935_7a30, - 0x773a_8ca5_196b_1380, - 0xd0fd_a172_174e_d023, - 0x6cb9_5e0f_a776_aead, - 0x0d22_d5a4_0cec_7cff, - ]), - Fp::from_raw_unchecked([ - 0xf727_e092_85fd_8519, - 0xdc9d_55a8_3017_897b, - 0x7549_d8bd_0578_94ae, - 0x1784_1961_3d90_d8f8, - 0xfce9_5ebd_eb5b_490a, - 0x0467_ffae_f23f_c49e, - ]), - Fp::from_raw_unchecked([ - 0xc176_9e6a_7c38_5f1b, - 0x79bc_930d_eac0_1c03, - 0x5461_c75a_23ed_e3b5, - 0x6e20_829e_5c23_0c45, - 0x828e_0f1e_772a_53cd, - 0x116a_efa7_4912_7bff, - ]), - Fp::from_raw_unchecked([ - 0x101c_10bf_2744_c10a, - 0xbbf1_8d05_3a6a_3154, - 0xa0ec_f39e_f026_f602, - 0xfc00_9d49_96dc_5153, - 0xb900_0209_d5bd_08d3, - 0x189e_5fe4_470c_d73c, - ]), - Fp::from_raw_unchecked([ - 0x7ebd_546c_a157_5ed2, - 0xe47d_5a98_1d08_1b55, - 0x57b2_b625_b6d4_ca21, - 0xb0a1_ba04_2285_20cc, - 0x9873_8983_c210_7ff3, - 0x13dd_dbc4_799d_81d6, - ]), - Fp::from_raw_unchecked([ - 0x0931_9f2e_3983_4935, - 0x039e_952c_bdb0_5c21, - 0x55ba_77a9_a2f7_6493, - 0xfd04_e3df_c608_6467, - 0xfb95_832e_7d78_742e, - 0x0ef9_c24e_ccaf_5e0e, - ]), - Fp::from_raw_unchecked([ - 0x7609_0000_0002_fffd, - 0xebf4_000b_c40c_0002, - 0x5f48_9857_53c7_58ba, - 0x77ce_5853_7052_5745, - 0x5c07_1a97_a256_ec6d, - 0x15f6_5ec3_fa80_e493, - ]), -]; - -const SSWU_ELLP_A: Fp = Fp::from_raw_unchecked([ - 0x2f65_aa0e_9af5_aa51, - 0x8646_4c2d_1e84_16c3, - 0xb85c_e591_b7bd_31e2, - 0x27e1_1c91_b5f2_4e7c, - 0x2837_6eda_6bfc_1835, - 0x1554_55c3_e507_1d85, -]); - -const SSWU_ELLP_B: Fp = Fp::from_raw_unchecked([ - 0xfb99_6971_fe22_a1e0, - 0x9aa9_3eb3_5b74_2d6f, - 0x8c47_6013_de99_c5c4, - 0x873e_27c3_a221_e571, - 0xca72_b5e4_5a52_d888, - 0x0682_4061_418a_386b, -]); - -const SSWU_XI: Fp = Fp::from_raw_unchecked([ - 0x886c_0000_0023_ffdc, - 0x0f70_008d_3090_001d, - 0x7767_2417_ed58_28c3, - 0x9dac_23e9_43dc_1740, - 0x5055_3f1b_9c13_1521, - 0x078c_712f_be0a_b6e8, -]); - -const SQRT_M_XI_CUBED: Fp = Fp::from_raw_unchecked([ - 0x43b5_71ca_d321_5f1f, - 0xccb4_60ef_1c70_2dc2, - 0x742d_884f_4f97_100b, - 0xdb2c_3e32_38a3_382b, - 0xe40f_3fa1_3fce_8f88, - 0x0073_a2af_9892_a2ff, -]); - -impl HashToField for Fp { - // ceil(log2(p)) = 381, m = 1, k = 128. - type InputLength = U64; - - fn from_okm(okm: &GenericArray) -> Fp { - const F_2_256: Fp = Fp::from_raw_unchecked([ - 0x075b_3cd7_c5ce_820f, - 0x3ec6_ba62_1c3e_db0b, - 0x168a_13d8_2bff_6bce, - 0x8766_3c4b_f8c4_49d2, - 0x15f3_4c83_ddc8_d830, - 0x0f96_28b4_9caa_2e85, - ]); - - let mut bs = [0u8; 48]; - bs[16..].copy_from_slice(&okm[..32]); - let db = Fp::from_bytes(&bs).unwrap(); - - bs[16..].copy_from_slice(&okm[32..]); - let da = Fp::from_bytes(&bs).unwrap(); - - db * F_2_256 + da - } -} - -impl Sgn0 for Fp { - fn sgn0(&self) -> Choice { - // Turn into canonical form by computing - // (a.R) / R = a - let tmp = Fp::montgomery_reduce( - self.0[0], self.0[1], self.0[2], self.0[3], self.0[4], self.0[5], 0, 0, 0, 0, 0, 0, - ); - Choice::from((tmp.0[0] & 1) as u8) - } -} - -/// Maps an element of [`Fp`] to a point on iso-G1. -/// -/// Implements [section 6.6.2 of `draft-irtf-cfrg-hash-to-curve-12`][sswu]. -/// -/// [sswu]: https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-12#section-6.6.2 -fn map_to_curve_simple_swu(u: &Fp) -> G1Projective { - let usq = u.square(); - let xi_usq = SSWU_XI * usq; - let xisq_u4 = xi_usq.square(); - let nd_common = xisq_u4 + xi_usq; // XI^2 * u^4 + XI * u^2 - let x_den = SSWU_ELLP_A * Fp::conditional_select(&(-nd_common), &SSWU_XI, nd_common.is_zero()); - let x0_num = SSWU_ELLP_B * (Fp::one() + nd_common); // B * (1 + (XI^2 * u^4 + XI * u^2)) - - // compute g(x0(u)) - let x_densq = x_den.square(); - let gx_den = x_densq * x_den; - // x0_num^3 + A * x0_num * x_den^2 + B * x_den^3 - let gx0_num = (x0_num.square() + SSWU_ELLP_A * x_densq) * x0_num + SSWU_ELLP_B * gx_den; - - // compute g(X0(u)) ^ ((p - 3) // 4) - let sqrt_candidate = { - let u_v = gx0_num * gx_den; // u*v - let vsq = gx_den.square(); // v^2 - u_v * chain_pm3div4(&(u_v * vsq)) // u v (u v^3) ^ ((p - 3) // 4) - }; - - let gx0_square = (sqrt_candidate.square() * gx_den).ct_eq(&gx0_num); // g(x0) is square - let x1_num = x0_num * xi_usq; - // sqrt(-XI**3) * u^3 g(x0) ^ ((p - 3) // 4) - let y1 = SQRT_M_XI_CUBED * usq * u * sqrt_candidate; - - let x_num = Fp::conditional_select(&x1_num, &x0_num, gx0_square); - let mut y = Fp::conditional_select(&y1, &sqrt_candidate, gx0_square); - // ensure sign of y and sign of u agree - y.conditional_negate(y.sgn0() ^ u.sgn0()); - - G1Projective { - x: x_num, - y: y * x_den, - z: x_den, - } -} - -/// Maps an iso-G1 point to a G1 point. -fn iso_map(u: &G1Projective) -> G1Projective { - const COEFFS: [&[Fp]; 4] = [&ISO11_XNUM, &ISO11_XDEN, &ISO11_YNUM, &ISO11_YDEN]; - - // unpack input point - let G1Projective { x, y, z } = *u; - - // xnum, xden, ynum, yden - let mut mapvals = [Fp::zero(); 4]; - - // pre-compute powers of z - let zpows = { - let mut zpows = [Fp::zero(); 15]; - zpows[0] = z; - for idx in 1..zpows.len() { - zpows[idx] = zpows[idx - 1] * z; - } - zpows - }; - - // compute map value by Horner's rule - for idx in 0..4 { - let coeff = COEFFS[idx]; - let clast = coeff.len() - 1; - mapvals[idx] = coeff[clast]; - for jdx in 0..clast { - mapvals[idx] = mapvals[idx] * x + zpows[jdx] * coeff[clast - 1 - jdx]; - } - } - - // x denominator is order 1 less than x numerator, so we need an extra factor of z - mapvals[1] *= z; - - // multiply result of Y map by the y-coord, y / z - mapvals[2] *= y; - mapvals[3] *= z; - - G1Projective { - x: mapvals[0] * mapvals[3], // xnum * yden, - y: mapvals[2] * mapvals[1], // ynum * xden, - z: mapvals[1] * mapvals[3], // xden * yden - } -} - -impl MapToCurve for G1Projective { - type Field = Fp; - - fn map_to_curve(u: &Fp) -> G1Projective { - let pt = map_to_curve_simple_swu(u); - iso_map(&pt) - } - - fn clear_h(&self) -> Self { - self.clear_cofactor() - } -} - -#[cfg(test)] -fn check_g1_prime(pt: &G1Projective) -> bool { - // (X : Y : Z)==(X/Z, Y/Z) is on E': y^2 = x^3 + A * x + B. - // y^2 z = (x^3) + A (x z^2) + B z^3 - let zsq = pt.z.square(); - (pt.y.square() * pt.z) - == (pt.x.square() * pt.x + SSWU_ELLP_A * pt.x * zsq + SSWU_ELLP_B * zsq * pt.z) -} - -#[test] -fn test_simple_swu_expected() { - // exceptional case: zero - let p = map_to_curve_simple_swu(&Fp::zero()); - let G1Projective { x, y, z } = &p; - let xo = Fp::from_raw_unchecked([ - 0xfb99_6971_fe22_a1e0, - 0x9aa9_3eb3_5b74_2d6f, - 0x8c47_6013_de99_c5c4, - 0x873e_27c3_a221_e571, - 0xca72_b5e4_5a52_d888, - 0x0682_4061_418a_386b, - ]); - let yo = Fp::from_raw_unchecked([ - 0xfd6f_ced8_7a7f_11a3, - 0x9a6b_314b_03c8_db31, - 0x41f8_5416_e0ea_b593, - 0xfeeb_089f_7e6e_c4d7, - 0x85a1_34c3_7ed1_278f, - 0x0575_c525_bb9f_74bb, - ]); - let zo = Fp::from_raw_unchecked([ - 0x7f67_4ea0_a891_5178, - 0xb0f9_45fc_13b8_fa65, - 0x4b46_759a_38e8_7d76, - 0x2e7a_9296_41bb_b6a1, - 0x1668_ddfa_462b_f6b6, - 0x0096_0e2e_d1cf_294c, - ]); - assert_eq!(x, &xo); - assert_eq!(y, &yo); - assert_eq!(z, &zo); - assert!(check_g1_prime(&p)); - - // exceptional case: sqrt(-1/XI) (positive) - let excp = Fp::from_raw_unchecked([ - 0x00f3_d047_7e91_edbf, - 0x08d6_621e_4ca8_dc69, - 0xb9cf_7927_b19b_9726, - 0xba13_3c99_6caf_a2ec, - 0xed2a_5ccd_5ca7_bb68, - 0x19cb_022f_8ee9_d73b, - ]); - let p = map_to_curve_simple_swu(&excp); - let G1Projective { x, y, z } = &p; - assert_eq!(x, &xo); - assert_eq!(y, &yo); - assert_eq!(z, &zo); - assert!(check_g1_prime(&p)); - - // exceptional case: sqrt(-1/XI) (negative) - let excp = Fp::from_raw_unchecked([ - 0xb90b_2fb8_816d_bcec, - 0x15d5_9de0_64ab_2396, - 0xad61_5979_4515_5efe, - 0xaa64_0eeb_86d5_6fd2, - 0x5df1_4ae8_e6a3_f16e, - 0x0036_0fba_aa96_0f5e, - ]); - let p = map_to_curve_simple_swu(&excp); - let G1Projective { x, y, z } = &p; - let myo = -yo; - assert_eq!(x, &xo); - assert_eq!(y, &myo); - assert_eq!(z, &zo); - assert!(check_g1_prime(&p)); - - let u = Fp::from_raw_unchecked([ - 0xa618_fa19_f7e2_eadc, - 0x93c7_f1fc_876b_a245, - 0xe2ed_4cc4_7b5c_0ae0, - 0xd49e_fa74_e4a8_d000, - 0xa0b2_3ba6_92b5_431c, - 0x0d15_51f2_d7d8_d193, - ]); - let xo = Fp::from_raw_unchecked([ - 0x2197_ca55_fab3_ba48, - 0x591d_eb39_f434_949a, - 0xf9df_7fb4_f1fa_6a08, - 0x59e3_c16a_9dfa_8fa5, - 0xe592_9b19_4aad_5f7a, - 0x130a_46a4_c61b_44ed, - ]); - let yo = Fp::from_raw_unchecked([ - 0xf721_5b58_c720_0ad0, - 0x8905_1631_3a4e_66bf, - 0xc903_1acc_8a36_19a8, - 0xea1f_9978_fde3_ffec, - 0x0548_f02d_6cfb_f472, - 0x1693_7557_3529_163f, - ]); - let zo = Fp::from_raw_unchecked([ - 0xf36f_eb2e_1128_ade0, - 0x42e2_2214_250b_cd94, - 0xb94f_6ba2_dddf_62d6, - 0xf56d_4392_782b_f0a2, - 0xb2d7_ce1e_c263_09e7, - 0x182b_57ed_6b99_f0a1, - ]); - let p = map_to_curve_simple_swu(&u); - let G1Projective { x, y, z } = &p; - assert_eq!(x, &xo); - assert_eq!(y, &yo); - assert_eq!(z, &zo); - assert!(check_g1_prime(&p)); -} - -#[test] -fn test_osswu_semirandom() { - use rand_core::SeedableRng; - let mut rng = rand_xorshift::XorShiftRng::from_seed([ - 0x59, 0x62, 0xbe, 0x5d, 0x76, 0x3d, 0x31, 0x8d, 0x17, 0xdb, 0x37, 0x32, 0x54, 0x06, 0xbc, - 0xe5, - ]); - for _ in 0..32 { - let input = Fp::random(&mut rng); - let p = map_to_curve_simple_swu(&input); - assert!(check_g1_prime(&p)); - - let p_iso = iso_map(&p); - assert!(bool::from(p_iso.is_on_curve())); - } -} - -// test vectors from the draft 10 RFC -#[test] -fn test_encode_to_curve_10() { - use crate::{ - g1::G1Affine, - hash_to_curve::{ExpandMsgXmd, HashToCurve}, - }; - use std::string::{String, ToString}; - - struct TestCase { - msg: &'static [u8], - expected: [&'static str; 2], - } - impl TestCase { - fn expected(&self) -> String { - self.expected[0].to_string() + self.expected[1] - } - } - - const DOMAIN: &[u8] = b"QUUX-V01-CS02-with-BLS12381G1_XMD:SHA-256_SSWU_NU_"; - - let cases = vec![ - TestCase { - msg: b"", - expected: [ - "184bb665c37ff561a89ec2122dd343f20e0f4cbcaec84e3c3052ea81d1834e192c426074b02ed3dca4e7676ce4ce48ba", - "04407b8d35af4dacc809927071fc0405218f1401a6d15af775810e4e460064bcc9468beeba82fdc751be70476c888bf3", - ], - }, - TestCase { - msg: b"abc", - expected: [ - "009769f3ab59bfd551d53a5f846b9984c59b97d6842b20a2c565baa167945e3d026a3755b6345df8ec7e6acb6868ae6d", - "1532c00cf61aa3d0ce3e5aa20c3b531a2abd2c770a790a2613818303c6b830ffc0ecf6c357af3317b9575c567f11cd2c", - ], - }, - TestCase { - msg: b"abcdef0123456789", - expected: [ - "1974dbb8e6b5d20b84df7e625e2fbfecb2cdb5f77d5eae5fb2955e5ce7313cae8364bc2fff520a6c25619739c6bdcb6a", - "15f9897e11c6441eaa676de141c8d83c37aab8667173cbe1dfd6de74d11861b961dccebcd9d289ac633455dfcc7013a3", - ] - }, - TestCase { - msg: b"q128_qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ - qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ - qqqqqqqqqqqqqqqqqqqqqqqqq", - expected: [ - "0a7a047c4a8397b3446450642c2ac64d7239b61872c9ae7a59707a8f4f950f101e766afe58223b3bff3a19a7f754027c", - "1383aebba1e4327ccff7cf9912bda0dbc77de048b71ef8c8a81111d71dc33c5e3aa6edee9cf6f5fe525d50cc50b77cc9", - ] - }, - TestCase { - msg: b"a512_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - expected: [ - "0e7a16a975904f131682edbb03d9560d3e48214c9986bd50417a77108d13dc957500edf96462a3d01e62dc6cd468ef11", - "0ae89e677711d05c30a48d6d75e76ca9fb70fe06c6dd6ff988683d89ccde29ac7d46c53bb97a59b1901abf1db66052db", - ] - } - ]; - - for case in cases { - let g = >>::encode_to_curve( - &case.msg, DOMAIN, - ); - let aff = G1Affine::from(g); - let g_uncompressed = aff.to_uncompressed(); - - assert_eq!(case.expected(), hex::encode(&g_uncompressed[..])); - } -} - -// test vectors from the draft 10 RFC -#[test] -fn test_hash_to_curve_10() { - use crate::{ - g1::G1Affine, - hash_to_curve::{ExpandMsgXmd, HashToCurve}, - }; - use std::string::{String, ToString}; - - struct TestCase { - msg: &'static [u8], - expected: [&'static str; 2], - } - impl TestCase { - fn expected(&self) -> String { - self.expected[0].to_string() + self.expected[1] - } - } - - const DOMAIN: &[u8] = b"QUUX-V01-CS02-with-BLS12381G1_XMD:SHA-256_SSWU_RO_"; - - let cases = vec![ - TestCase { - msg: b"", - expected: [ - "052926add2207b76ca4fa57a8734416c8dc95e24501772c814278700eed6d1e4e8cf62d9c09db0fac349612b759e79a1", - "08ba738453bfed09cb546dbb0783dbb3a5f1f566ed67bb6be0e8c67e2e81a4cc68ee29813bb7994998f3eae0c9c6a265", - ], - }, - TestCase { - msg: b"abc", - expected: [ - "03567bc5ef9c690c2ab2ecdf6a96ef1c139cc0b2f284dca0a9a7943388a49a3aee664ba5379a7655d3c68900be2f6903", - "0b9c15f3fe6e5cf4211f346271d7b01c8f3b28be689c8429c85b67af215533311f0b8dfaaa154fa6b88176c229f2885d" - ], - }, - TestCase { - msg: b"abcdef0123456789", - expected: [ - "11e0b079dea29a68f0383ee94fed1b940995272407e3bb916bbf268c263ddd57a6a27200a784cbc248e84f357ce82d98", - "03a87ae2caf14e8ee52e51fa2ed8eefe80f02457004ba4d486d6aa1f517c0889501dc7413753f9599b099ebcbbd2d709" - ] - }, - TestCase { - msg: b"q128_qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ - qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ - qqqqqqqqqqqqqqqqqqqqqqqqq", - expected: [ - "15f68eaa693b95ccb85215dc65fa81038d69629f70aeee0d0f677cf22285e7bf58d7cb86eefe8f2e9bc3f8cb84fac488", - "1807a1d50c29f430b8cafc4f8638dfeeadf51211e1602a5f184443076715f91bb90a48ba1e370edce6ae1062f5e6dd38" - ] - }, - TestCase { - msg: b"a512_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - expected: [ - "082aabae8b7dedb0e78aeb619ad3bfd9277a2f77ba7fad20ef6aabdc6c31d19ba5a6d12283553294c1825c4b3ca2dcfe", - "05b84ae5a942248eea39e1d91030458c40153f3b654ab7872d779ad1e942856a20c438e8d99bc8abfbf74729ce1f7ac8" - ] - } - ]; - - for case in cases { - let g = >>::hash_to_curve( - &case.msg, DOMAIN, - ); - let g_uncompressed = G1Affine::from(g).to_uncompressed(); - - assert_eq!(case.expected(), hex::encode(&g_uncompressed[..])); - } -} - -#[cfg(test)] -// p-1 / 2 -pub const P_M1_OVER2: Fp = Fp::from_raw_unchecked([ - 0xa1fa_ffff_fffe_5557, - 0x995b_fff9_76a3_fffe, - 0x03f4_1d24_d174_ceb4, - 0xf654_7998_c199_5dbd, - 0x778a_468f_507a_6034, - 0x0205_5993_1f7f_8103, -]); - -#[test] -fn test_sgn0() { - assert_eq!(bool::from(Fp::zero().sgn0()), false); - assert_eq!(bool::from(Fp::one().sgn0()), true); - assert_eq!(bool::from((-Fp::one()).sgn0()), false); - assert_eq!(bool::from((-Fp::zero()).sgn0()), false); - assert_eq!(bool::from(P_M1_OVER2.sgn0()), true); - - let p_p1_over2 = P_M1_OVER2 + Fp::one(); - assert_eq!(bool::from(p_p1_over2.sgn0()), false); - - let neg_p_p1_over2 = { - let mut tmp = p_p1_over2; - tmp.conditional_negate(Choice::from(1u8)); - tmp - }; - assert_eq!(neg_p_p1_over2, P_M1_OVER2); -} diff --git a/constantine/bls12_381/src/hash_to_curve/map_g2.rs b/constantine/bls12_381/src/hash_to_curve/map_g2.rs deleted file mode 100644 index b2cdb956d..000000000 --- a/constantine/bls12_381/src/hash_to_curve/map_g2.rs +++ /dev/null @@ -1,900 +0,0 @@ -//! Implementation of hash-to-curve for the G2 group - -use subtle::{Choice, ConditionallyNegatable, ConditionallySelectable, ConstantTimeEq}; - -use super::chain::chain_p2m9div16; -use super::{HashToField, MapToCurve, Sgn0}; -use crate::generic_array::{ - typenum::{U128, U64}, - GenericArray, -}; -use crate::{fp::Fp, fp2::Fp2, g2::G2Projective}; - -/// Coefficients of the 3-isogeny x map's numerator -const ISO3_XNUM: [Fp2; 4] = [ - Fp2 { - c0: Fp::from_raw_unchecked([ - 0x47f6_71c7_1ce0_5e62, - 0x06dd_5707_1206_393e, - 0x7c80_cd2a_f3fd_71a2, - 0x0481_03ea_9e6c_d062, - 0xc545_16ac_c8d0_37f6, - 0x1380_8f55_0920_ea41, - ]), - c1: Fp::from_raw_unchecked([ - 0x47f6_71c7_1ce0_5e62, - 0x06dd_5707_1206_393e, - 0x7c80_cd2a_f3fd_71a2, - 0x0481_03ea_9e6c_d062, - 0xc545_16ac_c8d0_37f6, - 0x1380_8f55_0920_ea41, - ]), - }, - Fp2 { - c0: Fp::zero(), - c1: Fp::from_raw_unchecked([ - 0x5fe5_5555_554c_71d0, - 0x873f_ffdd_236a_aaa3, - 0x6a6b_4619_b26e_f918, - 0x21c2_8884_0887_4945, - 0x2836_cda7_028c_abc5, - 0x0ac7_3310_a7fd_5abd, - ]), - }, - Fp2 { - c0: Fp::from_raw_unchecked([ - 0x0a0c_5555_5559_71c3, - 0xdb0c_0010_1f9e_aaae, - 0xb1fb_2f94_1d79_7997, - 0xd396_0742_ef41_6e1c, - 0xb700_40e2_c205_56f4, - 0x149d_7861_e581_393b, - ]), - c1: Fp::from_raw_unchecked([ - 0xaff2_aaaa_aaa6_38e8, - 0x439f_ffee_91b5_5551, - 0xb535_a30c_d937_7c8c, - 0x90e1_4442_0443_a4a2, - 0x941b_66d3_8146_55e2, - 0x0563_9988_53fe_ad5e, - ]), - }, - Fp2 { - c0: Fp::from_raw_unchecked([ - 0x40aa_c71c_71c7_25ed, - 0x1909_5555_7a84_e38e, - 0xd817_050a_8f41_abc3, - 0xd864_85d4_c87f_6fb1, - 0x696e_b479_f885_d059, - 0x198e_1a74_3280_02d2, - ]), - c1: Fp::zero(), - }, -]; - -/// Coefficients of the 3-isogeny x map's denominator -const ISO3_XDEN: [Fp2; 3] = [ - Fp2 { - c0: Fp::zero(), - c1: Fp::from_raw_unchecked([ - 0x1f3a_ffff_ff13_ab97, - 0xf25b_fc61_1da3_ff3e, - 0xca37_57cb_3819_b208, - 0x3e64_2736_6f8c_ec18, - 0x0397_7bc8_6095_b089, - 0x04f6_9db1_3f39_a952, - ]), - }, - Fp2 { - c0: Fp::from_raw_unchecked([ - 0x4476_0000_0027_552e, - 0xdcb8_009a_4348_0020, - 0x6f7e_e9ce_4a6e_8b59, - 0xb103_30b7_c0a9_5bc6, - 0x6140_b1fc_fb1e_54b7, - 0x0381_be09_7f0b_b4e1, - ]), - c1: Fp::from_raw_unchecked([ - 0x7588_ffff_ffd8_557d, - 0x41f3_ff64_6e0b_ffdf, - 0xf7b1_e8d2_ac42_6aca, - 0xb374_1acd_32db_b6f8, - 0xe9da_f5b9_482d_581f, - 0x167f_53e0_ba74_31b8, - ]), - }, - Fp2::one(), -]; - -/// Coefficients of the 3-isogeny y map's numerator -const ISO3_YNUM: [Fp2; 4] = [ - Fp2 { - c0: Fp::from_raw_unchecked([ - 0x96d8_f684_bdfc_77be, - 0xb530_e4f4_3b66_d0e2, - 0x184a_88ff_3796_52fd, - 0x57cb_23ec_fae8_04e1, - 0x0fd2_e39e_ada3_eba9, - 0x08c8_055e_31c5_d5c3, - ]), - c1: Fp::from_raw_unchecked([ - 0x96d8_f684_bdfc_77be, - 0xb530_e4f4_3b66_d0e2, - 0x184a_88ff_3796_52fd, - 0x57cb_23ec_fae8_04e1, - 0x0fd2_e39e_ada3_eba9, - 0x08c8_055e_31c5_d5c3, - ]), - }, - Fp2 { - c0: Fp::zero(), - c1: Fp::from_raw_unchecked([ - 0xbf0a_71c7_1c91_b406, - 0x4d6d_55d2_8b76_38fd, - 0x9d82_f98e_5f20_5aee, - 0xa27a_a27b_1d1a_18d5, - 0x02c3_b2b2_d293_8e86, - 0x0c7d_1342_0b09_807f, - ]), - }, - Fp2 { - c0: Fp::from_raw_unchecked([ - 0xd7f9_5555_5553_1c74, - 0x21cf_fff7_48da_aaa8, - 0x5a9a_d186_6c9b_be46, - 0x4870_a221_0221_d251, - 0x4a0d_b369_c0a3_2af1, - 0x02b1_ccc4_29ff_56af, - ]), - c1: Fp::from_raw_unchecked([ - 0xe205_aaaa_aaac_8e37, - 0xfcdc_0007_6879_5556, - 0x0c96_011a_8a15_37dd, - 0x1c06_a963_f163_406e, - 0x010d_f44c_82a8_81e6, - 0x174f_4526_0f80_8feb, - ]), - }, - Fp2 { - c0: Fp::from_raw_unchecked([ - 0xa470_bda1_2f67_f35c, - 0xc0fe_38e2_3327_b425, - 0xc9d3_d0f2_c6f0_678d, - 0x1c55_c993_5b5a_982e, - 0x27f6_c0e2_f074_6764, - 0x117c_5e6e_28aa_9054, - ]), - c1: Fp::zero(), - }, -]; - -/// Coefficients of the 3-isogeny y map's denominator -const ISO3_YDEN: [Fp2; 4] = [ - Fp2 { - c0: Fp::from_raw_unchecked([ - 0x0162_ffff_fa76_5adf, - 0x8f7b_ea48_0083_fb75, - 0x561b_3c22_59e9_3611, - 0x11e1_9fc1_a9c8_75d5, - 0xca71_3efc_0036_7660, - 0x03c6_a03d_41da_1151, - ]), - c1: Fp::from_raw_unchecked([ - 0x0162_ffff_fa76_5adf, - 0x8f7b_ea48_0083_fb75, - 0x561b_3c22_59e9_3611, - 0x11e1_9fc1_a9c8_75d5, - 0xca71_3efc_0036_7660, - 0x03c6_a03d_41da_1151, - ]), - }, - Fp2 { - c0: Fp::zero(), - c1: Fp::from_raw_unchecked([ - 0x5db0_ffff_fd3b_02c5, - 0xd713_f523_58eb_fdba, - 0x5ea6_0761_a84d_161a, - 0xbb2c_75a3_4ea6_c44a, - 0x0ac6_7359_21c1_119b, - 0x0ee3_d913_bdac_fbf6, - ]), - }, - Fp2 { - c0: Fp::from_raw_unchecked([ - 0x66b1_0000_003a_ffc5, - 0xcb14_00e7_64ec_0030, - 0xa73e_5eb5_6fa5_d106, - 0x8984_c913_a0fe_09a9, - 0x11e1_0afb_78ad_7f13, - 0x0542_9d0e_3e91_8f52, - ]), - c1: Fp::from_raw_unchecked([ - 0x534d_ffff_ffc4_aae6, - 0x5397_ff17_4c67_ffcf, - 0xbff2_73eb_870b_251d, - 0xdaf2_8271_5287_0915, - 0x393a_9cba_ca9e_2dc3, - 0x14be_74db_faee_5748, - ]), - }, - Fp2::one(), -]; - -const SSWU_ELLP_A: Fp2 = Fp2 { - c0: Fp::zero(), - c1: Fp::from_raw_unchecked([ - 0xe53a_0000_0313_5242, - 0x0108_0c0f_def8_0285, - 0xe788_9edb_e340_f6bd, - 0x0b51_3751_2631_0601, - 0x02d6_9857_17c7_44ab, - 0x1220_b4e9_79ea_5467, - ]), -}; - -const SSWU_ELLP_B: Fp2 = Fp2 { - c0: Fp::from_raw_unchecked([ - 0x22ea_0000_0cf8_9db2, - 0x6ec8_32df_7138_0aa4, - 0x6e1b_9440_3db5_a66e, - 0x75bf_3c53_a794_73ba, - 0x3dd3_a569_412c_0a34, - 0x125c_db5e_74dc_4fd1, - ]), - c1: Fp::from_raw_unchecked([ - 0x22ea_0000_0cf8_9db2, - 0x6ec8_32df_7138_0aa4, - 0x6e1b_9440_3db5_a66e, - 0x75bf_3c53_a794_73ba, - 0x3dd3_a569_412c_0a34, - 0x125c_db5e_74dc_4fd1, - ]), -}; - -const SSWU_XI: Fp2 = Fp2 { - c0: Fp::from_raw_unchecked([ - 0x87eb_ffff_fff9_555c, - 0x656f_ffe5_da8f_fffa, - 0x0fd0_7493_45d3_3ad2, - 0xd951_e663_0665_76f4, - 0xde29_1a3d_41e9_80d3, - 0x0815_664c_7dfe_040d, - ]), - c1: Fp::from_raw_unchecked([ - 0x43f5_ffff_fffc_aaae, - 0x32b7_fff2_ed47_fffd, - 0x07e8_3a49_a2e9_9d69, - 0xeca8_f331_8332_bb7a, - 0xef14_8d1e_a0f4_c069, - 0x040a_b326_3eff_0206, - ]), -}; - -const SSWU_ETAS: [Fp2; 4] = [ - Fp2 { - c0: Fp::from_raw_unchecked([ - 0x05e5_1466_8ac7_36d2, - 0x9089_b4d6_b84f_3ea5, - 0x603c_384c_224a_8b32, - 0xf325_7909_536a_fea6, - 0x5c5c_dbab_ae65_6d81, - 0x075b_fa08_63c9_87e9, - ]), - c1: Fp::from_raw_unchecked([ - 0x338d_9bfe_0808_7330, - 0x7b8e_48b2_bd83_cefe, - 0x530d_ad5d_306b_5be7, - 0x5a4d_7e8e_6c40_8b6d, - 0x6258_f7a6_232c_ab9b, - 0x0b98_5811_cce1_4db5, - ]), - }, - Fp2 { - c0: Fp::from_raw_unchecked([ - 0x8671_6401_f7f7_377b, - 0xa31d_b74b_f3d0_3101, - 0x1423_2543_c645_9a3c, - 0x0a29_ccf6_8744_8752, - 0xe8c2_b010_201f_013c, - 0x0e68_b9d8_6c9e_98e4, - ]), - c1: Fp::from_raw_unchecked([ - 0x05e5_1466_8ac7_36d2, - 0x9089_b4d6_b84f_3ea5, - 0x603c_384c_224a_8b32, - 0xf325_7909_536a_fea6, - 0x5c5c_dbab_ae65_6d81, - 0x075b_fa08_63c9_87e9, - ]), - }, - Fp2 { - c0: Fp::from_raw_unchecked([ - 0x718f_dad2_4ee1_d90f, - 0xa58c_025b_ed82_76af, - 0x0c3a_1023_0ab7_976f, - 0xf0c5_4df5_c8f2_75e1, - 0x4ec2_478c_28ba_f465, - 0x1129_373a_90c5_08e6, - ]), - c1: Fp::from_raw_unchecked([ - 0x019a_f5f9_80a3_680c, - 0x4ed7_da0e_6606_3afa, - 0x6003_5472_3b5d_9972, - 0x8b2f_958b_20d0_9d72, - 0x0474_938f_02d4_61db, - 0x0dcf_8b9e_0684_ab1c, - ]), - }, - Fp2 { - c0: Fp::from_raw_unchecked([ - 0xb864_0a06_7f5c_429f, - 0xcfd4_25f0_4b4d_c505, - 0x072d_7e2e_bb53_5cb1, - 0xd947_b5f9_d2b4_754d, - 0x46a7_1427_4077_4afb, - 0x0c31_864c_32fb_3b7e, - ]), - c1: Fp::from_raw_unchecked([ - 0x718f_dad2_4ee1_d90f, - 0xa58c_025b_ed82_76af, - 0x0c3a_1023_0ab7_976f, - 0xf0c5_4df5_c8f2_75e1, - 0x4ec2_478c_28ba_f465, - 0x1129_373a_90c5_08e6, - ]), - }, -]; - -const SSWU_RV1: Fp2 = Fp2 { - c0: Fp::from_raw_unchecked([ - 0x7bcf_a7a2_5aa3_0fda, - 0xdc17_dec1_2a92_7e7c, - 0x2f08_8dd8_6b4e_bef1, - 0xd1ca_2087_da74_d4a7, - 0x2da2_5966_96ce_bc1d, - 0x0e2b_7eed_bbfd_87d2, - ]), - c1: Fp::from_raw_unchecked([ - 0x7bcf_a7a2_5aa3_0fda, - 0xdc17_dec1_2a92_7e7c, - 0x2f08_8dd8_6b4e_bef1, - 0xd1ca_2087_da74_d4a7, - 0x2da2_5966_96ce_bc1d, - 0x0e2b_7eed_bbfd_87d2, - ]), -}; - -impl HashToField for Fp2 { - // ceil(log2(p)) = 381, m = 2, k = 128. - type InputLength = U128; - - fn from_okm(okm: &GenericArray) -> Fp2 { - let c0 = ::from_okm(GenericArray::::from_slice(&okm[..64])); - let c1 = ::from_okm(GenericArray::::from_slice(&okm[64..])); - Fp2 { c0, c1 } - } -} - -impl Sgn0 for Fp2 { - fn sgn0(&self) -> Choice { - let sign_0 = self.c0.sgn0(); - let zero_0 = self.c0.is_zero(); - let sign_1 = self.c1.sgn0(); - sign_0 | (zero_0 & sign_1) - } -} - -/// Maps from an [`Fp2]` element to a point on iso-G2. -fn map_to_curve_simple_swu(u: &Fp2) -> G2Projective { - let usq = u.square(); - let xi_usq = SSWU_XI * usq; - let xisq_u4 = xi_usq.square(); - let nd_common = xisq_u4 + xi_usq; // XI^2 * u^4 + XI * u^2 - let x_den = SSWU_ELLP_A * Fp2::conditional_select(&(-nd_common), &SSWU_XI, nd_common.is_zero()); - let x0_num = SSWU_ELLP_B * (Fp2::one() + nd_common); // B * (1 + (XI^2 * u^4 + XI * u^2)) - - // compute g(x0(u)) - let x_densq = x_den.square(); - let gx_den = x_densq * x_den; - // x0_num^3 + A * x0_num * x_den^2 + B * x_den^3 - let gx0_num = (x0_num.square() + SSWU_ELLP_A * x_densq) * x0_num + SSWU_ELLP_B * gx_den; - - // compute g(x0(u)) ^ ((p^2 - 9) // 16) - let sqrt_candidate = { - let vsq = gx_den.square(); // v^2 - let v_3 = vsq * gx_den; // v^3 - let v_4 = vsq.square(); // v^4 - let uv_7 = gx0_num * v_3 * v_4; // u v^7 - let uv_15 = uv_7 * v_4.square(); // u v^15 - uv_7 * chain_p2m9div16(&uv_15) // u v^7 (u v^15) ^ ((p^2 - 9) // 16) - }; - - // set y = sqrt_candidate * Fp2::one(), check candidate against other roots of unity - let mut y = sqrt_candidate; - // check Fp2(0, 1) - let tmp = Fp2 { - c0: -sqrt_candidate.c1, - c1: sqrt_candidate.c0, - }; - y.conditional_assign(&tmp, (tmp.square() * gx_den).ct_eq(&gx0_num)); - // check Fp2(RV1, RV1) - let tmp = sqrt_candidate * SSWU_RV1; - y.conditional_assign(&tmp, (tmp.square() * gx_den).ct_eq(&gx0_num)); - // check Fp2(RV1, -RV1) - let tmp = Fp2 { - c0: tmp.c1, - c1: -tmp.c0, - }; - y.conditional_assign(&tmp, (tmp.square() * gx_den).ct_eq(&gx0_num)); - - // compute g(x1(u)) = g(x0(u)) * XI^3 * u^6 - let gx1_num = gx0_num * xi_usq * xisq_u4; - // compute g(x1(u)) * u^3 - let sqrt_candidate = sqrt_candidate * usq * u; - let mut eta_found = Choice::from(0u8); - for eta in &SSWU_ETAS[..] { - let tmp = sqrt_candidate * eta; - let found = (tmp.square() * gx_den).ct_eq(&gx1_num); - y.conditional_assign(&tmp, found); - eta_found |= found; - } - - let x_num = Fp2::conditional_select(&x0_num, &(x0_num * xi_usq), eta_found); - // ensure sign of y and sign of u agree - y.conditional_negate(u.sgn0() ^ y.sgn0()); - - G2Projective { - x: x_num, - y: y * x_den, - z: x_den, - } -} - -/// Maps from an iso-G2 point to a G2 point. -fn iso_map(u: &G2Projective) -> G2Projective { - const COEFFS: [&[Fp2]; 4] = [&ISO3_XNUM, &ISO3_XDEN, &ISO3_YNUM, &ISO3_YDEN]; - - // unpack input point - let G2Projective { x, y, z } = *u; - - // xnum, xden, ynum, yden - let mut mapvals = [Fp2::zero(); 4]; - - // compute powers of z - let zsq = z.square(); - let zpows = [z, zsq, zsq * z]; - - // compute map value by Horner's rule - for idx in 0..4 { - let coeff = COEFFS[idx]; - let clast = coeff.len() - 1; - mapvals[idx] = coeff[clast]; - for jdx in 0..clast { - mapvals[idx] = mapvals[idx] * x + zpows[jdx] * coeff[clast - 1 - jdx]; - } - } - - // x denominator is order 1 less than x numerator, so we need an extra factor of z - mapvals[1] *= z; - - // multiply result of Y map by the y-coord, y / z - mapvals[2] *= y; - mapvals[3] *= z; - - G2Projective { - x: mapvals[0] * mapvals[3], // xnum * yden, - y: mapvals[2] * mapvals[1], // ynum * xden, - z: mapvals[1] * mapvals[3], // xden * yden - } -} - -impl MapToCurve for G2Projective { - type Field = Fp2; - - fn map_to_curve(u: &Fp2) -> G2Projective { - let pt = map_to_curve_simple_swu(u); - iso_map(&pt) - } - - fn clear_h(&self) -> Self { - self.clear_cofactor() - } -} - -#[cfg(test)] -fn check_g2_prime(pt: &G2Projective) -> bool { - // (X : Y : Z)==(X/Z, Y/Z) is on E': y^2 = x^3 + A * x + B. - // y^2 z = (x^3) + A (x z^2) + B z^3 - let zsq = pt.z.square(); - (pt.y.square() * pt.z) - == (pt.x.square() * pt.x + SSWU_ELLP_A * pt.x * zsq + SSWU_ELLP_B * zsq * pt.z) -} - -#[test] -fn test_osswu_semirandom() { - use rand_core::SeedableRng; - let mut rng = rand_xorshift::XorShiftRng::from_seed([ - 0x59, 0x62, 0xbe, 0x5d, 0x76, 0x3d, 0x31, 0x8d, 0x17, 0xdb, 0x37, 0x32, 0x54, 0x06, 0xbc, - 0xe5, - ]); - for _ in 0..32 { - let input = Fp2::random(&mut rng); - let p = map_to_curve_simple_swu(&input); - assert!(check_g2_prime(&p)); - - let p_iso = iso_map(&p); - assert!(bool::from(p_iso.is_on_curve())); - } -} - -// test vectors from the draft 10 RFC -#[test] -fn test_encode_to_curve_10() { - use crate::{ - g2::G2Affine, - hash_to_curve::{ExpandMsgXmd, HashToCurve}, - }; - use std::string::{String, ToString}; - - struct TestCase { - msg: &'static [u8], - expected: [&'static str; 4], - } - impl TestCase { - fn expected(&self) -> String { - self.expected[0].to_string() + self.expected[1] + self.expected[2] + self.expected[3] - } - } - - const DOMAIN: &[u8] = b"QUUX-V01-CS02-with-BLS12381G2_XMD:SHA-256_SSWU_NU_"; - - let cases = vec![ - TestCase { - msg: b"", - expected: [ - "126b855e9e69b1f691f816e48ac6977664d24d99f8724868a184186469ddfd4617367e94527d4b74fc86413483afb35b", - "00e7f4568a82b4b7dc1f14c6aaa055edf51502319c723c4dc2688c7fe5944c213f510328082396515734b6612c4e7bb7", - "1498aadcf7ae2b345243e281ae076df6de84455d766ab6fcdaad71fab60abb2e8b980a440043cd305db09d283c895e3d", - "0caead0fd7b6176c01436833c79d305c78be307da5f6af6c133c47311def6ff1e0babf57a0fb5539fce7ee12407b0a42", - ], - }, - TestCase { - msg: b"abc", - expected: [ - "0296238ea82c6d4adb3c838ee3cb2346049c90b96d602d7bb1b469b905c9228be25c627bffee872def773d5b2a2eb57d", - "108ed59fd9fae381abfd1d6bce2fd2fa220990f0f837fa30e0f27914ed6e1454db0d1ee957b219f61da6ff8be0d6441f", - "153606c417e59fb331b7ae6bce4fbf7c5190c33ce9402b5ebe2b70e44fca614f3f1382a3625ed5493843d0b0a652fc3f", - "033f90f6057aadacae7963b0a0b379dd46750c1c94a6357c99b65f63b79e321ff50fe3053330911c56b6ceea08fee656", - ], - }, - TestCase { - msg: b"abcdef0123456789", - expected: [ - "0da75be60fb6aa0e9e3143e40c42796edf15685cafe0279afd2a67c3dff1c82341f17effd402e4f1af240ea90f4b659b", - "038af300ef34c7759a6caaa4e69363cafeed218a1f207e93b2c70d91a1263d375d6730bd6b6509dcac3ba5b567e85bf3", - "0492f4fed741b073e5a82580f7c663f9b79e036b70ab3e51162359cec4e77c78086fe879b65ca7a47d34374c8315ac5e", - "19b148cbdf163cf0894f29660d2e7bfb2b68e37d54cc83fd4e6e62c020eaa48709302ef8e746736c0e19342cc1ce3df4", - ] - }, - TestCase { - msg: b"q128_qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ - qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ - qqqqqqqqqqqqqqqqqqqqqqqqq", - expected: [ - "12c8c05c1d5fc7bfa847f4d7d81e294e66b9a78bc9953990c358945e1f042eedafce608b67fdd3ab0cb2e6e263b9b1ad", - "0c5ae723be00e6c3f0efe184fdc0702b64588fe77dda152ab13099a3bacd3876767fa7bbad6d6fd90b3642e902b208f9", - "11c624c56dbe154d759d021eec60fab3d8b852395a89de497e48504366feedd4662d023af447d66926a28076813dd646", - "04e77ddb3ede41b5ec4396b7421dd916efc68a358a0d7425bddd253547f2fb4830522358491827265dfc5bcc1928a569", - ] - }, - TestCase { - msg: b"a512_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - expected: [ - "1565c2f625032d232f13121d3cfb476f45275c303a037faa255f9da62000c2c864ea881e2bcddd111edc4a3c0da3e88d", - "0ea4e7c33d43e17cc516a72f76437c4bf81d8f4eac69ac355d3bf9b71b8138d55dc10fd458be115afa798b55dac34be1", - "0f8991d2a1ad662e7b6f58ab787947f1fa607fce12dde171bc17903b012091b657e15333e11701edcf5b63ba2a561247", - "043b6f5fe4e52c839148dc66f2b3751e69a0f6ebb3d056d6465d50d4108543ecd956e10fa1640dfd9bc0030cc2558d28", - ] - } - ]; - - for case in cases { - let g = >>::encode_to_curve( - &case.msg, DOMAIN, - ); - let g_uncompressed = G2Affine::from(g).to_uncompressed(); - - assert_eq!(case.expected(), hex::encode(&g_uncompressed[..])); - } -} - -// test vectors from the draft 10 RFC -#[test] -fn test_hash_to_curve_10() { - use crate::{ - g2::G2Affine, - hash_to_curve::{ExpandMsgXmd, HashToCurve}, - }; - use std::string::{String, ToString}; - - struct TestCase { - msg: &'static [u8], - expected: [&'static str; 4], - } - impl TestCase { - fn expected(&self) -> String { - self.expected[0].to_string() + self.expected[1] + self.expected[2] + self.expected[3] - } - } - - const DOMAIN: &[u8] = b"QUUX-V01-CS02-with-BLS12381G2_XMD:SHA-256_SSWU_RO_"; - - let cases = vec![ - TestCase { - msg: b"", - expected: [ - "05cb8437535e20ecffaef7752baddf98034139c38452458baeefab379ba13dff5bf5dd71b72418717047f5b0f37da03d", - "0141ebfbdca40eb85b87142e130ab689c673cf60f1a3e98d69335266f30d9b8d4ac44c1038e9dcdd5393faf5c41fb78a", - "12424ac32561493f3fe3c260708a12b7c620e7be00099a974e259ddc7d1f6395c3c811cdd19f1e8dbf3e9ecfdcbab8d6", - "0503921d7f6a12805e72940b963c0cf3471c7b2a524950ca195d11062ee75ec076daf2d4bc358c4b190c0c98064fdd92", - ], - }, - TestCase { - msg: b"abc", - expected: [ - "139cddbccdc5e91b9623efd38c49f81a6f83f175e80b06fc374de9eb4b41dfe4ca3a230ed250fbe3a2acf73a41177fd8", - "02c2d18e033b960562aae3cab37a27ce00d80ccd5ba4b7fe0e7a210245129dbec7780ccc7954725f4168aff2787776e6", - "00aa65dae3c8d732d10ecd2c50f8a1baf3001578f71c694e03866e9f3d49ac1e1ce70dd94a733534f106d4cec0eddd16", - "1787327b68159716a37440985269cf584bcb1e621d3a7202be6ea05c4cfe244aeb197642555a0645fb87bf7466b2ba48", - ], - }, - TestCase { - msg: b"abcdef0123456789", - expected: [ - "190d119345b94fbd15497bcba94ecf7db2cbfd1e1fe7da034d26cbba169fb3968288b3fafb265f9ebd380512a71c3f2c", - "121982811d2491fde9ba7ed31ef9ca474f0e1501297f68c298e9f4c0028add35aea8bb83d53c08cfc007c1e005723cd0", - "0bb5e7572275c567462d91807de765611490205a941a5a6af3b1691bfe596c31225d3aabdf15faff860cb4ef17c7c3be", - "05571a0f8d3c08d094576981f4a3b8eda0a8e771fcdcc8ecceaf1356a6acf17574518acb506e435b639353c2e14827c8", - ] - }, - TestCase { - msg: b"q128_qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ - qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\ - qqqqqqqqqqqqqqqqqqqqqqqqq", - expected: [ - "0934aba516a52d8ae479939a91998299c76d39cc0c035cd18813bec433f587e2d7a4fef038260eef0cef4d02aae3eb91", - "19a84dd7248a1066f737cc34502ee5555bd3c19f2ecdb3c7d9e24dc65d4e25e50d83f0f77105e955d78f4762d33c17da", - "09bcccfa036b4847c9950780733633f13619994394c23ff0b32fa6b795844f4a0673e20282d07bc69641cee04f5e5662", - "14f81cd421617428bc3b9fe25afbb751d934a00493524bc4e065635b0555084dd54679df1536101b2c979c0152d09192", - ] - }, - TestCase { - msg: b"a512_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - expected: [ - "11fca2ff525572795a801eed17eb12785887c7b63fb77a42be46ce4a34131d71f7a73e95fee3f812aea3de78b4d01569", - "01a6ba2f9a11fa5598b2d8ace0fbe0a0eacb65deceb476fbbcb64fd24557c2f4b18ecfc5663e54ae16a84f5ab7f62534", - "03a47f8e6d1763ba0cad63d6114c0accbef65707825a511b251a660a9b3994249ae4e63fac38b23da0c398689ee2ab52", - "0b6798718c8aed24bc19cb27f866f1c9effcdbf92397ad6448b5c9db90d2b9da6cbabf48adc1adf59a1a28344e79d57e", - ] - } - ]; - - for case in cases { - let g = >>::hash_to_curve( - &case.msg, DOMAIN, - ); - let g_uncompressed = G2Affine::from(g).to_uncompressed(); - - assert_eq!(case.expected(), hex::encode(&g_uncompressed[..])); - } -} - -#[test] -fn test_sgn0() { - use super::map_g1::P_M1_OVER2; - - assert_eq!(bool::from(Fp2::zero().sgn0()), false); - assert_eq!(bool::from(Fp2::one().sgn0()), true); - assert_eq!( - bool::from( - Fp2 { - c0: P_M1_OVER2, - c1: Fp::zero() - } - .sgn0() - ), - true - ); - assert_eq!( - bool::from( - Fp2 { - c0: P_M1_OVER2, - c1: Fp::one() - } - .sgn0() - ), - true - ); - assert_eq!( - bool::from( - Fp2 { - c0: Fp::zero(), - c1: P_M1_OVER2, - } - .sgn0() - ), - true - ); - assert_eq!( - bool::from( - Fp2 { - c0: Fp::one(), - c1: P_M1_OVER2, - } - .sgn0() - ), - true - ); - - let p_p1_over2 = P_M1_OVER2 + Fp::one(); - assert_eq!( - bool::from( - Fp2 { - c0: p_p1_over2, - c1: Fp::zero() - } - .sgn0() - ), - false - ); - assert_eq!( - bool::from( - Fp2 { - c0: p_p1_over2, - c1: Fp::one() - } - .sgn0() - ), - false - ); - assert_eq!( - bool::from( - Fp2 { - c0: Fp::zero(), - c1: p_p1_over2, - } - .sgn0() - ), - false - ); - assert_eq!( - bool::from( - Fp2 { - c0: Fp::one(), - c1: p_p1_over2, - } - .sgn0() - ), - true - ); - - assert_eq!( - bool::from( - Fp2 { - c0: P_M1_OVER2, - c1: -Fp::one() - } - .sgn0() - ), - true - ); - assert_eq!( - bool::from( - Fp2 { - c0: p_p1_over2, - c1: -Fp::one() - } - .sgn0() - ), - false - ); - assert_eq!( - bool::from( - Fp2 { - c0: Fp::zero(), - c1: -Fp::one() - } - .sgn0() - ), - false - ); - assert_eq!( - bool::from( - Fp2 { - c0: P_M1_OVER2, - c1: p_p1_over2 - } - .sgn0() - ), - true - ); - assert_eq!( - bool::from( - Fp2 { - c0: p_p1_over2, - c1: P_M1_OVER2 - } - .sgn0() - ), - false - ); - - assert_eq!( - bool::from( - Fp2 { - c0: -Fp::one(), - c1: P_M1_OVER2, - } - .sgn0() - ), - false - ); - assert_eq!( - bool::from( - Fp2 { - c0: -Fp::one(), - c1: p_p1_over2, - } - .sgn0() - ), - false - ); - assert_eq!( - bool::from( - Fp2 { - c0: -Fp::one(), - c1: Fp::zero(), - } - .sgn0() - ), - false - ); - assert_eq!( - bool::from( - Fp2 { - c0: p_p1_over2, - c1: P_M1_OVER2, - } - .sgn0() - ), - false - ); - assert_eq!( - bool::from( - Fp2 { - c0: P_M1_OVER2, - c1: p_p1_over2, - } - .sgn0() - ), - true - ); -} diff --git a/constantine/bls12_381/src/hash_to_curve/map_scalar.rs b/constantine/bls12_381/src/hash_to_curve/map_scalar.rs deleted file mode 100644 index ec4bb9e09..000000000 --- a/constantine/bls12_381/src/hash_to_curve/map_scalar.rs +++ /dev/null @@ -1,39 +0,0 @@ -//! Implementation of hash-to-field for Scalar values - -use super::HashToField; -use crate::generic_array::{typenum::U48, GenericArray}; -use crate::scalar::Scalar; - -impl HashToField for Scalar { - // ceil(log2(p)) = 255, m = 1, k = 128. - type InputLength = U48; - - fn from_okm(okm: &GenericArray) -> Scalar { - let mut bs = [0u8; 64]; - bs[16..].copy_from_slice(okm); - bs.reverse(); // into little endian - Scalar::from_bytes_wide(&bs) - } -} - -#[test] -fn test_hash_to_scalar() { - let tests: &[(&[u8], &str)] = &[ - ( - &[0u8; 48], - "0x0000000000000000000000000000000000000000000000000000000000000000", - ), - ( - b"aaaaaabbbbbbccccccddddddeeeeeeffffffgggggghhhhhh", - "0x2228450bf55d8fe62395161bd3677ff6fc28e45b89bc87e02a818eda11a8c5da", - ), - ( - b"111111222222333333444444555555666666777777888888", - "0x4aa543cbd2f0c8f37f8a375ce2e383eb343e7e3405f61e438b0a15fb8899d1ae", - ), - ]; - for (input, expected) in tests { - let output = format!("{:?}", Scalar::from_okm(GenericArray::from_slice(input))); - assert_eq!(&output, expected); - } -} diff --git a/constantine/bls12_381/src/hash_to_curve/mod.rs b/constantine/bls12_381/src/hash_to_curve/mod.rs deleted file mode 100644 index 4c15a498a..000000000 --- a/constantine/bls12_381/src/hash_to_curve/mod.rs +++ /dev/null @@ -1,113 +0,0 @@ -//! This module implements hash_to_curve, hash_to_field and related -//! hashing primitives for use with BLS signatures. - -use core::ops::Add; - -use subtle::Choice; - -pub(crate) mod chain; - -mod expand_msg; -pub use self::expand_msg::{ - ExpandMessage, ExpandMessageState, ExpandMsgXmd, ExpandMsgXof, InitExpandMessage, -}; - -mod map_g1; -mod map_g2; -mod map_scalar; - -use crate::generic_array::{typenum::Unsigned, ArrayLength, GenericArray}; - -/// Enables a byte string to be hashed into one or more field elements for a given curve. -/// -/// Implements [section 5 of `draft-irtf-cfrg-hash-to-curve-12`][hash_to_field]. -/// -/// [hash_to_field]: https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-12#section-5 -pub trait HashToField: Sized { - /// The length of the data used to produce an individual field element. - /// - /// This must be set to `m * L = m * ceil((ceil(log2(p)) + k) / 8)`, where `p` is the - /// characteristic of `Self`, `m` is the extension degree of `Self`, and `k` is the - /// security parameter. - type InputLength: ArrayLength; - - /// Interprets the given output keying material as a big endian integer, and reduces - /// it into a field element. - fn from_okm(okm: &GenericArray) -> Self; - - /// Hashes a byte string of arbitrary length into one or more elements of `Self`, - /// using [`ExpandMessage`] variant `X`. - /// - /// Implements [section 5.3 of `draft-irtf-cfrg-hash-to-curve-12`][hash_to_field]. - /// - /// [hash_to_field]: https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-12#section-5.3 - fn hash_to_field(message: &[u8], dst: &[u8], output: &mut [Self]) { - let len_per_elm = Self::InputLength::to_usize(); - let len_in_bytes = output.len() * len_per_elm; - let mut expander = X::init_expand(message, dst, len_in_bytes); - - let mut buf = GenericArray::::default(); - output.iter_mut().for_each(|item| { - expander.read_into(&mut buf[..]); - *item = Self::from_okm(&buf); - }); - } -} - -/// Allow conversion from the output of hashed or encoded input into points on the curve -pub trait MapToCurve: Sized { - /// The field element type. - type Field: Copy + Default + HashToField; - - /// Maps an element of the finite field `Self::Field` to a point on the curve `Self`. - fn map_to_curve(elt: &Self::Field) -> Self; - - /// Clears the cofactor, sending a point on curve E to the target group (G1/G2). - fn clear_h(&self) -> Self; -} - -/// Implementation of random oracle maps to the curve. -pub trait HashToCurve: MapToCurve + for<'a> Add<&'a Self, Output = Self> { - /// Implements a uniform encoding from byte strings to elements of `Self`. - /// - /// This function is suitable for most applications requiring a random - /// oracle returning points in `Self`. - fn hash_to_curve(message: impl AsRef<[u8]>, dst: &[u8]) -> Self { - let mut u = [Self::Field::default(); 2]; - Self::Field::hash_to_field::(message.as_ref(), dst, &mut u); - let p1 = Self::map_to_curve(&u[0]); - let p2 = Self::map_to_curve(&u[1]); - (p1 + &p2).clear_h() - } - - /// Implements a **non-uniform** encoding from byte strings to elements of `Self`. - /// - /// The distribution of its output is not uniformly random in `Self`: the set of - /// possible outputs of this function is only a fraction of the points in `Self`, and - /// some elements of this set are more likely to be output than others. See - /// [section 10.1 of `draft-irtf-cfrg-hash-to-curve-12`][encode_to_curve-distribution] - /// for a more precise definition of `encode_to_curve`'s output distribution. - /// - /// [encode_to_curve-distribution]: https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-12#section-10.1 - fn encode_to_curve(message: impl AsRef<[u8]>, dst: &[u8]) -> Self { - let mut u = [Self::Field::default(); 1]; - Self::Field::hash_to_field::(message.as_ref(), dst, &mut u); - let p = Self::map_to_curve(&u[0]); - p.clear_h() - } -} - -impl HashToCurve for G -where - G: MapToCurve + for<'a> Add<&'a Self, Output = Self>, - X: ExpandMessage, -{ -} - -pub(crate) trait Sgn0 { - /// Returns either 0 or 1 indicating the "sign" of x, where sgn0(x) == 1 - /// just when x is "negative". (In other words, this function always considers 0 to be positive.) - /// - /// The equivalent for draft 6 would be `lexicographically_largest`. - fn sgn0(&self) -> Choice; -} diff --git a/constantine/bls12_381/src/lib.rs b/constantine/bls12_381/src/lib.rs deleted file mode 100644 index 6b8ce4c4f..000000000 --- a/constantine/bls12_381/src/lib.rs +++ /dev/null @@ -1,97 +0,0 @@ -//! # `bls12_381` -//! -//! This crate provides an implementation of the BLS12-381 pairing-friendly elliptic -//! curve construction. -//! -//! * **This implementation has not been reviewed or audited. Use at your own risk.** -//! * This implementation targets Rust `1.36` or later. -//! * This implementation does not require the Rust standard library. -//! * All operations are constant time unless explicitly noted. - -#![no_std] -#![cfg_attr(docsrs, feature(doc_cfg))] -// Catch documentation errors caused by code changes. -#![deny(rustdoc::broken_intra_doc_links)] -#![deny(missing_debug_implementations)] -#![deny(unsafe_code)] -#![allow(clippy::too_many_arguments)] -#![allow(clippy::many_single_char_names)] -// This lint is described at -// https://rust-lang.github.io/rust-clippy/master/index.html#suspicious_arithmetic_impl -// In our library, some of the arithmetic involving extension fields will necessarily -// involve various binary operators, and so this lint is triggered unnecessarily. -#![allow(clippy::suspicious_arithmetic_impl)] - -#[cfg(feature = "alloc")] -extern crate alloc; - -#[cfg(test)] -#[macro_use] -extern crate std; - -#[cfg(test)] -#[cfg(feature = "groups")] -mod tests; - -#[macro_use] -mod util; - -/// Notes about how the BLS12-381 elliptic curve is designed, specified -/// and implemented by this library. -pub mod notes { - pub mod design; - pub mod serialization; -} - -pub mod scalar; - -pub use scalar::Scalar; - -#[cfg(feature = "groups")] -mod fp; -#[cfg(feature = "groups")] -pub mod fp2; -#[cfg(feature = "groups")] -pub mod g1; -#[cfg(feature = "groups")] -pub mod g2; - -#[cfg(feature = "groups")] -pub use g1::{G1Affine, G1Projective}; -#[cfg(feature = "groups")] -pub use g2::{G2Affine, G2Projective}; - -#[cfg(feature = "groups")] -mod fp12; -#[cfg(feature = "groups")] -mod fp6; - -// The BLS parameter x for BLS12-381 is -0xd201000000010000 -#[cfg(feature = "groups")] -const BLS_X: u64 = 0xd201_0000_0001_0000; -#[cfg(feature = "groups")] -const BLS_X_IS_NEGATIVE: bool = true; - -pub const MODULUS: Scalar = scalar::MODULUS; -pub const R2: Scalar = scalar::R2; - -#[cfg(feature = "pairings")] -mod pairings; - -#[cfg(feature = "pairings")] -pub use pairings::{pairing, Bls12, Gt, MillerLoopResult}; - -#[cfg(all(feature = "pairings", feature = "alloc"))] -pub use pairings::{multi_miller_loop, G2Prepared}; - -pub use fp::Fp; -pub use fp2::Fp2; -pub use fp6::Fp6; -pub use fp12::Fp12; - -/// Use the generic_array re-exported by digest to avoid a version mismatch -#[cfg(feature = "experimental")] -pub(crate) use digest::generic_array; - -#[cfg(feature = "experimental")] -pub mod hash_to_curve; diff --git a/constantine/bls12_381/src/notes/design.rs b/constantine/bls12_381/src/notes/design.rs deleted file mode 100644 index 685a7e75f..000000000 --- a/constantine/bls12_381/src/notes/design.rs +++ /dev/null @@ -1,108 +0,0 @@ -//! # Design of BLS12-381 -//! ## Fixed Generators -//! -//! Although any generator produced by hashing to $\mathbb{G}_1$ or $\mathbb{G}_2$ is -//! safe to use in a cryptographic protocol, we specify some simple, fixed generators. -//! -//! In order to derive these generators, we select the lexicographically smallest -//! valid $x$-coordinate and the lexicographically smallest corresponding $y$-coordinate, -//! and then scale the resulting point by the cofactor, such that the result is not the -//! identity. This results in the following fixed generators: -//! -//! 1. $\mathbb{G}_1$ -//! * $x = 3685416753713387016781088315183077757961620795782546409894578378688607592378376318836054947676345821548104185464507$ -//! * $y = 1339506544944476473020471379941921221584933875938349620426543736416511423956333506472724655353366534992391756441569$ -//! 2. $\mathbb{G}_2$ -//! * $x = 352701069587466618187139116011060144890029952792775240219908644239793785735715026873347600343865175952761926303160 + 3059144344244213709971259814753781636986470325476647558659373206291635324768958432433509563104347017837885763365758 u$ -//! * $y = 1985150602287291935568054521177171638300868978215655730859378665066344726373823718423869104263333984641494340347905 + 927553665492332455747201965776037880757740193453592970025027978793976877002675564980949289727957565575433344219582 u$ -//! -//! This can be derived using the following sage script: -//! -//! ```text -//! param = -0xd201000000010000 -//! def r(x): -//! return (x**4) - (x**2) + 1 -//! def q(x): -//! return (((x - 1) ** 2) * ((x**4) - (x**2) + 1) // 3) + x -//! def g1_h(x): -//! return ((x-1)**2) // 3 -//! def g2_h(x): -//! return ((x**8) - (4 * (x**7)) + (5 * (x**6)) - (4 * (x**4)) + (6 * (x**3)) - (4 * (x**2)) - (4*x) + 13) // 9 -//! q = q(param) -//! r = r(param) -//! Fq = GF(q) -//! ec = EllipticCurve(Fq, [0, 4]) -//! def psqrt(v): -//! assert(not v.is_zero()) -//! a = sqrt(v) -//! b = -a -//! if a < b: -//! return a -//! else: -//! return b -//! for x in range(0,100): -//! rhs = Fq(x)^3 + 4 -//! if rhs.is_square(): -//! y = psqrt(rhs) -//! p = ec(x, y) * g1_h(param) -//! if (not p.is_zero()) and (p * r).is_zero(): -//! print("g1 generator: {}".format(p)) -//! break -//! Fq2. = GF(q^2, modulus=[1, 0, 1]) -//! ec2 = EllipticCurve(Fq2, [0, (4 * (1 + i))]) -//! assert(ec2.order() == (r * g2_h(param))) -//! for x in range(0,100): -//! rhs = (Fq2(x))^3 + (4 * (1 + i)) -//! if rhs.is_square(): -//! y = psqrt(rhs) -//! p = ec2(Fq2(x), y) * g2_h(param) -//! if not p.is_zero() and (p * r).is_zero(): -//! print("g2 generator: {}".format(p)) -//! break -//! ``` -//! -//! ## Nontrivial third root of unity -//! -//! To use the fast subgroup check algorithm for $\mathbb{G_1}$ from https://eprint.iacr.org/2019/814.pdf and -//! https://eprint.iacr.org/2021/1130, it is necessary to find a nontrivial cube root of -//! unity ฮฒ in Fp to define the endomorphism: -//! $$(x, y) \rightarrow (\beta x, y)$$ -//! which is equivalent to -//! $$P \rightarrow \lambda P$$ -//! where $\lambda$, a nontrivial cube root of unity in Fr, satisfies $\lambda^2 + \lambda +1 = 0 \pmod{r}. -//! -//! $$\beta = 793479390729215512621379701633421447060886740281060493010456487427281649075476305620758731620350$$ -//! can be derived using the following sage commands after running the above sage script: -//! -//! ```text -//! # Prints the given field element in Montgomery form. -//! def print_fq(a): -//! R = 1 << 384 -//! tmp = ZZ(Fq(a*R)) -//! while tmp > 0: -//! print("0x{:_x}, ".format(tmp % (1 << 64))) -//! tmp >>= 64 -//! ฮฒ = (Fq.multiplicative_generator() ** ((q-1)/3)) -//! print_fq(ฮฒ) -//! ``` -//! -//! ## Psi -//! -//! To use the fast subgroup check algorithm for $\mathbb{G_2}$ from https://eprint.iacr.org/2019/814.pdf and -//! https://eprint.iacr.org/2021/1130, it is necessary to find the endomorphism: -//! -//! $$(x, y, z) \rightarrow (x^q \psi_x, y^q \psi_y, z^q)$$ -//! -//! where: -//! -//! 1. $\psi_x = 1 / ((i+1) ^ ((q-1)/3)) \in \mathbb{F}_{q^2}$, and -//! 2. $\psi_y = 1 / ((i+1) ^ ((q-1)/2)) \in \mathbb{F}_{q^2}$ -//! -//! can be derived using the following sage commands after running the above script and commands: -//! ```text -//! psi_x = (1/((i+1)**((q-1)/3))) -//! psi_y = (1/((i+1)**((q-1)/2))) -//! print_fq(psi_x.polynomial().coefficients()[0]) -//! print_fq(psi_y.polynomial().coefficients()[0]) -//! print_fq(psi_y.polynomial().coefficients()[1]) -//! ``` diff --git a/constantine/bls12_381/src/notes/serialization.rs b/constantine/bls12_381/src/notes/serialization.rs deleted file mode 100644 index ded752e6e..000000000 --- a/constantine/bls12_381/src/notes/serialization.rs +++ /dev/null @@ -1,29 +0,0 @@ -//! # BLS12-381 serialization -//! -//! * $\mathbb{F}\_p$ elements are encoded in big-endian form. They occupy 48 -//! bytes in this form. -//! * $\mathbb{F}\_{p^2}$ elements are encoded in big-endian form, meaning that -//! the $\mathbb{F}\_{p^2}$ element $c\_0 + c\_1 \cdot u$ is represented by the -//! $\mathbb{F}\_p$ element $c\_1$ followed by the $\mathbb{F}\_p$ element $c\_0$. -//! This means $\mathbb{F}_{p^2}$ elements occupy 96 bytes in this form. -//! * The group $\mathbb{G}\_1$ uses $\mathbb{F}\_p$ elements for coordinates. The -//! group $\mathbb{G}\_2$ uses $\mathbb{F}_{p^2}$ elements for coordinates. -//! * $\mathbb{G}\_1$ and $\mathbb{G}\_2$ elements can be encoded in uncompressed -//! form (the x-coordinate followed by the y-coordinate) or in compressed form -//! (just the x-coordinate). $\mathbb{G}\_1$ elements occupy 96 bytes in -//! uncompressed form, and 48 bytes in compressed form. $\mathbb{G}\_2$ -//! elements occupy 192 bytes in uncompressed form, and 96 bytes in compressed -//! form. -//! -//! The most-significant three bits of a $\mathbb{G}\_1$ or $\mathbb{G}\_2$ -//! encoding should be masked away before the coordinate(s) are interpreted. -//! These bits are used to unambiguously represent the underlying element: -//! * The most significant bit, when set, indicates that the point is in -//! compressed form. Otherwise, the point is in uncompressed form. -//! * The second-most significant bit indicates that the point is at infinity. -//! If this bit is set, the remaining bits of the group element's encoding -//! should be set to zero. -//! * The third-most significant bit is set if (and only if) this point is in -//! compressed form _and_ it is not the point at infinity _and_ its -//! y-coordinate is the lexicographically largest of the two associated with -//! the encoded x-coordinate. diff --git a/constantine/bls12_381/src/pairings.rs b/constantine/bls12_381/src/pairings.rs deleted file mode 100644 index 2a9d923a2..000000000 --- a/constantine/bls12_381/src/pairings.rs +++ /dev/null @@ -1,970 +0,0 @@ -use crate::fp::Fp; -use crate::fp12::Fp12; -use crate::fp2::Fp2; -use crate::fp6::Fp6; -use crate::{G1Affine, G1Projective, G2Affine, G2Projective, Scalar, BLS_X, BLS_X_IS_NEGATIVE}; - -use core::borrow::Borrow; -use core::fmt; -use core::iter::Sum; -use core::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}; -use group::Group; -use pairing::{Engine, PairingCurveAffine}; -use rand_core::RngCore; -use subtle::{Choice, ConditionallySelectable, ConstantTimeEq}; - -#[cfg(feature = "alloc")] -use alloc::vec::Vec; -#[cfg(feature = "alloc")] -use pairing::MultiMillerLoop; - -/// Represents results of a Miller loop, one of the most expensive portions -/// of the pairing function. `MillerLoopResult`s cannot be compared with each -/// other until `.final_exponentiation()` is called, which is also expensive. -#[cfg_attr(docsrs, doc(cfg(feature = "pairings")))] -#[derive(Copy, Clone, Debug)] -pub struct MillerLoopResult(pub(crate) Fp12); - -impl Default for MillerLoopResult { - fn default() -> Self { - MillerLoopResult(Fp12::one()) - } -} - -#[cfg(feature = "zeroize")] -impl zeroize::DefaultIsZeroes for MillerLoopResult {} - -impl ConditionallySelectable for MillerLoopResult { - fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self { - MillerLoopResult(Fp12::conditional_select(&a.0, &b.0, choice)) - } -} - -impl MillerLoopResult { - /// This performs a "final exponentiation" routine to convert the result - /// of a Miller loop into an element of `Gt` with help of efficient squaring - /// operation in the so-called `cyclotomic subgroup` of `Fq6` so that - /// it can be compared with other elements of `Gt`. - pub fn final_exponentiation(&self) -> Gt { - #[must_use] - fn fp4_square(a: Fp2, b: Fp2) -> (Fp2, Fp2) { - let t0 = a.square(); - let t1 = b.square(); - let mut t2 = t1.mul_by_nonresidue(); - let c0 = t2 + t0; - t2 = a + b; - t2 = t2.square(); - t2 -= t0; - let c1 = t2 - t1; - - (c0, c1) - } - // Adaptation of Algorithm 5.5.4, Guide to Pairing-Based Cryptography - // Faster Squaring in the Cyclotomic Subgroup of Sixth Degree Extensions - // https://eprint.iacr.org/2009/565.pdf - #[must_use] - fn cyclotomic_square(f: Fp12) -> Fp12 { - let mut z0 = f.c0.c0; - let mut z4 = f.c0.c1; - let mut z3 = f.c0.c2; - let mut z2 = f.c1.c0; - let mut z1 = f.c1.c1; - let mut z5 = f.c1.c2; - - let (t0, t1) = fp4_square(z0, z1); - - // For A - z0 = t0 - z0; - z0 = z0 + z0 + t0; - - z1 = t1 + z1; - z1 = z1 + z1 + t1; - - let (mut t0, t1) = fp4_square(z2, z3); - let (t2, t3) = fp4_square(z4, z5); - - // For C - z4 = t0 - z4; - z4 = z4 + z4 + t0; - - z5 = t1 + z5; - z5 = z5 + z5 + t1; - - // For B - t0 = t3.mul_by_nonresidue(); - z2 = t0 + z2; - z2 = z2 + z2 + t0; - - z3 = t2 - z3; - z3 = z3 + z3 + t2; - - Fp12 { - c0: Fp6 { - c0: z0, - c1: z4, - c2: z3, - }, - c1: Fp6 { - c0: z2, - c1: z1, - c2: z5, - }, - } - } - #[must_use] - fn cycolotomic_exp(f: Fp12) -> Fp12 { - let x = BLS_X; - let mut tmp = Fp12::one(); - let mut found_one = false; - for i in (0..64).rev().map(|b| ((x >> b) & 1) == 1) { - if found_one { - tmp = cyclotomic_square(tmp) - } else { - found_one = i; - } - - if i { - tmp *= f; - } - } - - tmp.conjugate() - } - - let mut f = self.0; - let mut t0 = f - .frobenius_map() - .frobenius_map() - .frobenius_map() - .frobenius_map() - .frobenius_map() - .frobenius_map(); - Gt(f.invert() - .map(|mut t1| { - let mut t2 = t0 * t1; - t1 = t2; - t2 = t2.frobenius_map().frobenius_map(); - t2 *= t1; - t1 = cyclotomic_square(t2).conjugate(); - let mut t3 = cycolotomic_exp(t2); - let mut t4 = cyclotomic_square(t3); - let mut t5 = t1 * t3; - t1 = cycolotomic_exp(t5); - t0 = cycolotomic_exp(t1); - let mut t6 = cycolotomic_exp(t0); - t6 *= t4; - t4 = cycolotomic_exp(t6); - t5 = t5.conjugate(); - t4 *= t5 * t2; - t5 = t2.conjugate(); - t1 *= t2; - t1 = t1.frobenius_map().frobenius_map().frobenius_map(); - t6 *= t5; - t6 = t6.frobenius_map(); - t3 *= t0; - t3 = t3.frobenius_map().frobenius_map(); - t3 *= t1; - t3 *= t6; - f = t3 * t4; - - f - }) - // We unwrap() because `MillerLoopResult` can only be constructed - // by a function within this crate, and we uphold the invariant - // that the enclosed value is nonzero. - .unwrap()) - } -} - -impl<'a, 'b> Add<&'b MillerLoopResult> for &'a MillerLoopResult { - type Output = MillerLoopResult; - - #[inline] - fn add(self, rhs: &'b MillerLoopResult) -> MillerLoopResult { - MillerLoopResult(self.0 * rhs.0) - } -} - -impl_add_binop_specify_output!(MillerLoopResult, MillerLoopResult, MillerLoopResult); - -impl AddAssign for MillerLoopResult { - #[inline] - fn add_assign(&mut self, rhs: MillerLoopResult) { - *self = *self + rhs; - } -} - -impl<'b> AddAssign<&'b MillerLoopResult> for MillerLoopResult { - #[inline] - fn add_assign(&mut self, rhs: &'b MillerLoopResult) { - *self = *self + rhs; - } -} - -/// This is an element of $\mathbb{G}_T$, the target group of the pairing function. As with -/// $\mathbb{G}_1$ and $\mathbb{G}_2$ this group has order $q$. -/// -/// Typically, $\mathbb{G}_T$ is written multiplicatively but we will write it additively to -/// keep code and abstractions consistent. -#[cfg_attr(docsrs, doc(cfg(feature = "pairings")))] -#[derive(Copy, Clone, Debug)] -pub struct Gt(pub Fp12); - -impl Default for Gt { - fn default() -> Self { - Self::identity() - } -} - -#[cfg(feature = "zeroize")] -impl zeroize::DefaultIsZeroes for Gt {} - -impl fmt::Display for Gt { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{:?}", self) - } -} - -impl ConstantTimeEq for Gt { - fn ct_eq(&self, other: &Self) -> Choice { - self.0.ct_eq(&other.0) - } -} - -impl ConditionallySelectable for Gt { - fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self { - Gt(Fp12::conditional_select(&a.0, &b.0, choice)) - } -} - -impl Eq for Gt {} -impl PartialEq for Gt { - #[inline] - fn eq(&self, other: &Self) -> bool { - bool::from(self.ct_eq(other)) - } -} - -impl Gt { - /// Returns the group identity, which is $1$. - pub fn identity() -> Gt { - Gt(Fp12::one()) - } - - /// Doubles this group element. - pub fn double(&self) -> Gt { - Gt(self.0.square()) - } -} - -impl<'a> Neg for &'a Gt { - type Output = Gt; - - #[inline] - fn neg(self) -> Gt { - // The element is unitary, so we just conjugate. - Gt(self.0.conjugate()) - } -} - -impl Neg for Gt { - type Output = Gt; - - #[inline] - fn neg(self) -> Gt { - -&self - } -} - -impl<'a, 'b> Add<&'b Gt> for &'a Gt { - type Output = Gt; - - #[inline] - fn add(self, rhs: &'b Gt) -> Gt { - Gt(self.0 * rhs.0) - } -} - -impl<'a, 'b> Sub<&'b Gt> for &'a Gt { - type Output = Gt; - - #[inline] - fn sub(self, rhs: &'b Gt) -> Gt { - self + (-rhs) - } -} - -impl<'a, 'b> Mul<&'b Scalar> for &'a Gt { - type Output = Gt; - - fn mul(self, other: &'b Scalar) -> Self::Output { - let mut acc = Gt::identity(); - - // This is a simple double-and-add implementation of group element - // multiplication, moving from most significant to least - // significant bit of the scalar. - // - // We skip the leading bit because it's always unset for Fq - // elements. - for bit in other - .to_bytes() - .iter() - .rev() - .flat_map(|byte| (0..8).rev().map(move |i| Choice::from((byte >> i) & 1u8))) - .skip(1) - { - acc = acc.double(); - acc = Gt::conditional_select(&acc, &(acc + self), bit); - } - - acc - } -} - -impl_binops_additive!(Gt, Gt); -impl_binops_multiplicative!(Gt, Scalar); - -impl Sum for Gt -where - T: Borrow, -{ - fn sum(iter: I) -> Self - where - I: Iterator, - { - iter.fold(Self::identity(), |acc, item| acc + item.borrow()) - } -} - -impl Group for Gt { - type Scalar = Scalar; - - fn random(mut rng: impl RngCore) -> Self { - loop { - let inner = Fp12::random(&mut rng); - - // Not all elements of Fp12 are elements of the prime-order multiplicative - // subgroup. We run the random element through final_exponentiation to obtain - // a valid element, which requires that it is non-zero. - if !bool::from(inner.is_zero()) { - return MillerLoopResult(inner).final_exponentiation(); - } - } - } - - fn identity() -> Self { - Self::identity() - } - - fn generator() -> Self { - // pairing(&G1Affine::generator(), &G2Affine::generator()) - Gt(Fp12 { - c0: Fp6 { - c0: Fp2 { - c0: Fp::from_raw_unchecked([ - 0x1972_e433_a01f_85c5, - 0x97d3_2b76_fd77_2538, - 0xc8ce_546f_c96b_cdf9, - 0xcef6_3e73_66d4_0614, - 0xa611_3427_8184_3780, - 0x13f3_448a_3fc6_d825, - ]), - c1: Fp::from_raw_unchecked([ - 0xd263_31b0_2e9d_6995, - 0x9d68_a482_f779_7e7d, - 0x9c9b_2924_8d39_ea92, - 0xf480_1ca2_e131_07aa, - 0xa16c_0732_bdbc_b066, - 0x083c_a4af_ba36_0478, - ]), - }, - c1: Fp2 { - c0: Fp::from_raw_unchecked([ - 0x59e2_61db_0916_b641, - 0x2716_b6f4_b23e_960d, - 0xc8e5_5b10_a0bd_9c45, - 0x0bdb_0bd9_9c4d_eda8, - 0x8cf8_9ebf_57fd_aac5, - 0x12d6_b792_9e77_7a5e, - ]), - c1: Fp::from_raw_unchecked([ - 0x5fc8_5188_b0e1_5f35, - 0x34a0_6e3a_8f09_6365, - 0xdb31_26a6_e02a_d62c, - 0xfc6f_5aa9_7d9a_990b, - 0xa12f_55f5_eb89_c210, - 0x1723_703a_926f_8889, - ]), - }, - c2: Fp2 { - c0: Fp::from_raw_unchecked([ - 0x9358_8f29_7182_8778, - 0x43f6_5b86_11ab_7585, - 0x3183_aaf5_ec27_9fdf, - 0xfa73_d7e1_8ac9_9df6, - 0x64e1_76a6_a64c_99b0, - 0x179f_a78c_5838_8f1f, - ]), - c1: Fp::from_raw_unchecked([ - 0x672a_0a11_ca2a_ef12, - 0x0d11_b9b5_2aa3_f16b, - 0xa444_12d0_699d_056e, - 0xc01d_0177_221a_5ba5, - 0x66e0_cede_6c73_5529, - 0x05f5_a71e_9fdd_c339, - ]), - }, - }, - c1: Fp6 { - c0: Fp2 { - c0: Fp::from_raw_unchecked([ - 0xd30a_88a1_b062_c679, - 0x5ac5_6a5d_35fc_8304, - 0xd0c8_34a6_a81f_290d, - 0xcd54_30c2_da37_07c7, - 0xf0c2_7ff7_8050_0af0, - 0x0924_5da6_e2d7_2eae, - ]), - c1: Fp::from_raw_unchecked([ - 0x9f2e_0676_791b_5156, - 0xe2d1_c823_4918_fe13, - 0x4c9e_459f_3c56_1bf4, - 0xa3e8_5e53_b9d3_e3c1, - 0x820a_121e_21a7_0020, - 0x15af_6183_41c5_9acc, - ]), - }, - c1: Fp2 { - c0: Fp::from_raw_unchecked([ - 0x7c95_658c_2499_3ab1, - 0x73eb_3872_1ca8_86b9, - 0x5256_d749_4774_34bc, - 0x8ba4_1902_ea50_4a8b, - 0x04a3_d3f8_0c86_ce6d, - 0x18a6_4a87_fb68_6eaa, - ]), - c1: Fp::from_raw_unchecked([ - 0xbb83_e71b_b920_cf26, - 0x2a52_77ac_92a7_3945, - 0xfc0e_e59f_94f0_46a0, - 0x7158_cdf3_7860_58f7, - 0x7cc1_061b_82f9_45f6, - 0x03f8_47aa_9fdb_e567, - ]), - }, - c2: Fp2 { - c0: Fp::from_raw_unchecked([ - 0x8078_dba5_6134_e657, - 0x1cd7_ec9a_4399_8a6e, - 0xb1aa_599a_1a99_3766, - 0xc9a0_f62f_0842_ee44, - 0x8e15_9be3_b605_dffa, - 0x0c86_ba0d_4af1_3fc2, - ]), - c1: Fp::from_raw_unchecked([ - 0xe80f_f2a0_6a52_ffb1, - 0x7694_ca48_721a_906c, - 0x7583_183e_03b0_8514, - 0xf567_afdd_40ce_e4e2, - 0x9a6d_96d2_e526_a5fc, - 0x197e_9f49_861f_2242, - ]), - }, - }, - }) - } - - fn is_identity(&self) -> Choice { - self.ct_eq(&Self::identity()) - } - - #[must_use] - fn double(&self) -> Self { - self.double() - } -} - -#[cfg(feature = "alloc")] -#[cfg_attr(docsrs, doc(cfg(all(feature = "pairings", feature = "alloc"))))] -#[derive(Clone, Debug)] -/// This structure contains cached computations pertaining to a $\mathbb{G}_2$ -/// element as part of the pairing function (specifically, the Miller loop) and -/// so should be computed whenever a $\mathbb{G}_2$ element is being used in -/// multiple pairings or is otherwise known in advance. This should be used in -/// conjunction with the [`multi_miller_loop`](crate::multi_miller_loop) -/// function provided by this crate. -/// -/// Requires the `alloc` and `pairing` crate features to be enabled. -pub struct G2Prepared { - infinity: Choice, - coeffs: Vec<(Fp2, Fp2, Fp2)>, -} - -#[cfg(feature = "alloc")] -impl From for G2Prepared { - fn from(q: G2Affine) -> G2Prepared { - struct Adder { - cur: G2Projective, - base: G2Affine, - coeffs: Vec<(Fp2, Fp2, Fp2)>, - } - - impl MillerLoopDriver for Adder { - type Output = (); - - fn doubling_step(&mut self, _: Self::Output) -> Self::Output { - let coeffs = doubling_step(&mut self.cur); - self.coeffs.push(coeffs); - } - fn addition_step(&mut self, _: Self::Output) -> Self::Output { - let coeffs = addition_step(&mut self.cur, &self.base); - self.coeffs.push(coeffs); - } - fn square_output(_: Self::Output) -> Self::Output {} - fn conjugate(_: Self::Output) -> Self::Output {} - fn one() -> Self::Output {} - } - - let is_identity = q.is_identity(); - let q = G2Affine::conditional_select(&q, &G2Affine::generator(), is_identity); - - let mut adder = Adder { - cur: G2Projective::from(q), - base: q, - coeffs: Vec::with_capacity(68), - }; - - miller_loop(&mut adder); - - assert_eq!(adder.coeffs.len(), 68); - - G2Prepared { - infinity: is_identity, - coeffs: adder.coeffs, - } - } -} - -#[cfg(feature = "alloc")] -#[cfg_attr(docsrs, doc(cfg(all(feature = "pairings", feature = "alloc"))))] -/// Computes $$\sum_{i=1}^n \textbf{ML}(a_i, b_i)$$ given a series of terms -/// $$(a_1, b_1), (a_2, b_2), ..., (a_n, b_n).$$ -/// -/// Requires the `alloc` and `pairing` crate features to be enabled. -pub fn multi_miller_loop(terms: &[(&G1Affine, &G2Prepared)]) -> MillerLoopResult { - struct Adder<'a, 'b, 'c> { - terms: &'c [(&'a G1Affine, &'b G2Prepared)], - index: usize, - } - - impl<'a, 'b, 'c> MillerLoopDriver for Adder<'a, 'b, 'c> { - type Output = Fp12; - - fn doubling_step(&mut self, mut f: Self::Output) -> Self::Output { - let index = self.index; - for term in self.terms { - let either_identity = term.0.is_identity() | term.1.infinity; - - let new_f = ell(f, &term.1.coeffs[index], term.0); - f = Fp12::conditional_select(&new_f, &f, either_identity); - } - self.index += 1; - - f - } - fn addition_step(&mut self, mut f: Self::Output) -> Self::Output { - let index = self.index; - for term in self.terms { - let either_identity = term.0.is_identity() | term.1.infinity; - - let new_f = ell(f, &term.1.coeffs[index], term.0); - f = Fp12::conditional_select(&new_f, &f, either_identity); - } - self.index += 1; - - f - } - fn square_output(f: Self::Output) -> Self::Output { - f.square() - } - fn conjugate(f: Self::Output) -> Self::Output { - f.conjugate() - } - fn one() -> Self::Output { - Fp12::one() - } - } - - let mut adder = Adder { terms, index: 0 }; - - let tmp = miller_loop(&mut adder); - - MillerLoopResult(tmp) -} - -/// Invoke the pairing function without the use of precomputation and other optimizations. -#[cfg_attr(docsrs, doc(cfg(feature = "pairings")))] -pub fn pairing(p: &G1Affine, q: &G2Affine) -> Gt { - struct Adder { - cur: G2Projective, - base: G2Affine, - p: G1Affine, - } - - impl MillerLoopDriver for Adder { - type Output = Fp12; - - fn doubling_step(&mut self, f: Self::Output) -> Self::Output { - let coeffs = doubling_step(&mut self.cur); - ell(f, &coeffs, &self.p) - } - fn addition_step(&mut self, f: Self::Output) -> Self::Output { - let coeffs = addition_step(&mut self.cur, &self.base); - ell(f, &coeffs, &self.p) - } - fn square_output(f: Self::Output) -> Self::Output { - f.square() - } - fn conjugate(f: Self::Output) -> Self::Output { - f.conjugate() - } - fn one() -> Self::Output { - Fp12::one() - } - } - - let either_identity = p.is_identity() | q.is_identity(); - let p = G1Affine::conditional_select(p, &G1Affine::generator(), either_identity); - let q = G2Affine::conditional_select(q, &G2Affine::generator(), either_identity); - - let mut adder = Adder { - cur: G2Projective::from(q), - base: q, - p, - }; - - let tmp = miller_loop(&mut adder); - let tmp = MillerLoopResult(Fp12::conditional_select( - &tmp, - &Fp12::one(), - either_identity, - )); - tmp.final_exponentiation() -} - -trait MillerLoopDriver { - type Output; - - fn doubling_step(&mut self, f: Self::Output) -> Self::Output; - fn addition_step(&mut self, f: Self::Output) -> Self::Output; - fn square_output(f: Self::Output) -> Self::Output; - fn conjugate(f: Self::Output) -> Self::Output; - fn one() -> Self::Output; -} - -/// This is a "generic" implementation of the Miller loop to avoid duplicating code -/// structure elsewhere; instead, we'll write concrete instantiations of -/// `MillerLoopDriver` for whatever purposes we need (such as caching modes). -fn miller_loop(driver: &mut D) -> D::Output { - let mut f = D::one(); - - let mut found_one = false; - for i in (0..64).rev().map(|b| (((BLS_X >> 1) >> b) & 1) == 1) { - if !found_one { - found_one = i; - continue; - } - - f = driver.doubling_step(f); - - if i { - f = driver.addition_step(f); - } - - f = D::square_output(f); - } - - f = driver.doubling_step(f); - - if BLS_X_IS_NEGATIVE { - f = D::conjugate(f); - } - - f -} - -fn ell(f: Fp12, coeffs: &(Fp2, Fp2, Fp2), p: &G1Affine) -> Fp12 { - let mut c0 = coeffs.0; - let mut c1 = coeffs.1; - - c0.c0 *= p.y; - c0.c1 *= p.y; - - c1.c0 *= p.x; - c1.c1 *= p.x; - - f.mul_by_014(&coeffs.2, &c1, &c0) -} - -fn doubling_step(r: &mut G2Projective) -> (Fp2, Fp2, Fp2) { - // Adaptation of Algorithm 26, https://eprint.iacr.org/2010/354.pdf - let tmp0 = r.x.square(); - let tmp1 = r.y.square(); - let tmp2 = tmp1.square(); - let tmp3 = (tmp1 + r.x).square() - tmp0 - tmp2; - let tmp3 = tmp3 + tmp3; - let tmp4 = tmp0 + tmp0 + tmp0; - let tmp6 = r.x + tmp4; - let tmp5 = tmp4.square(); - let zsquared = r.z.square(); - r.x = tmp5 - tmp3 - tmp3; - r.z = (r.z + r.y).square() - tmp1 - zsquared; - r.y = (tmp3 - r.x) * tmp4; - let tmp2 = tmp2 + tmp2; - let tmp2 = tmp2 + tmp2; - let tmp2 = tmp2 + tmp2; - r.y -= tmp2; - let tmp3 = tmp4 * zsquared; - let tmp3 = tmp3 + tmp3; - let tmp3 = -tmp3; - let tmp6 = tmp6.square() - tmp0 - tmp5; - let tmp1 = tmp1 + tmp1; - let tmp1 = tmp1 + tmp1; - let tmp6 = tmp6 - tmp1; - let tmp0 = r.z * zsquared; - let tmp0 = tmp0 + tmp0; - - (tmp0, tmp3, tmp6) -} - -fn addition_step(r: &mut G2Projective, q: &G2Affine) -> (Fp2, Fp2, Fp2) { - // Adaptation of Algorithm 27, https://eprint.iacr.org/2010/354.pdf - let zsquared = r.z.square(); - let ysquared = q.y.square(); - let t0 = zsquared * q.x; - let t1 = ((q.y + r.z).square() - ysquared - zsquared) * zsquared; - let t2 = t0 - r.x; - let t3 = t2.square(); - let t4 = t3 + t3; - let t4 = t4 + t4; - let t5 = t4 * t2; - let t6 = t1 - r.y - r.y; - let t9 = t6 * q.x; - let t7 = t4 * r.x; - r.x = t6.square() - t5 - t7 - t7; - r.z = (r.z + t2).square() - zsquared - t3; - let t10 = q.y + r.z; - let t8 = (t7 - r.x) * t6; - let t0 = r.y * t5; - let t0 = t0 + t0; - r.y = t8 - t0; - let t10 = t10.square() - ysquared; - let ztsquared = r.z.square(); - let t10 = t10 - ztsquared; - let t9 = t9 + t9 - t10; - let t10 = r.z + r.z; - let t6 = -t6; - let t1 = t6 + t6; - - (t10, t1, t9) -} - -impl PairingCurveAffine for G1Affine { - type Pair = G2Affine; - type PairingResult = Gt; - - fn pairing_with(&self, other: &Self::Pair) -> Self::PairingResult { - pairing(self, other) - } -} - -impl PairingCurveAffine for G2Affine { - type Pair = G1Affine; - type PairingResult = Gt; - - fn pairing_with(&self, other: &Self::Pair) -> Self::PairingResult { - pairing(other, self) - } -} - -/// A [`pairing::Engine`] for BLS12-381 pairing operations. -#[cfg_attr(docsrs, doc(cfg(feature = "pairings")))] -#[derive(Clone, Debug)] -pub struct Bls12; - -impl Engine for Bls12 { - type Fr = Scalar; - type G1 = G1Projective; - type G1Affine = G1Affine; - type G2 = G2Projective; - type G2Affine = G2Affine; - type Gt = Gt; - - fn pairing(p: &Self::G1Affine, q: &Self::G2Affine) -> Self::Gt { - pairing(p, q) - } -} - -impl pairing::MillerLoopResult for MillerLoopResult { - type Gt = Gt; - - fn final_exponentiation(&self) -> Self::Gt { - self.final_exponentiation() - } -} - -#[cfg(feature = "alloc")] -impl MultiMillerLoop for Bls12 { - type G2Prepared = G2Prepared; - type Result = MillerLoopResult; - - fn multi_miller_loop(terms: &[(&Self::G1Affine, &Self::G2Prepared)]) -> Self::Result { - multi_miller_loop(terms) - } -} - -#[test] -fn test_gt_generator() { - assert_eq!( - Gt::generator(), - pairing(&G1Affine::generator(), &G2Affine::generator()) - ); -} - -#[test] -fn test_bilinearity() { - use crate::Scalar; - - let a = Scalar::from_raw([1, 2, 3, 4]).invert().unwrap().square(); - let b = Scalar::from_raw([5, 6, 7, 8]).invert().unwrap().square(); - let c = a * b; - - let g = G1Affine::from(G1Affine::generator() * a); - let h = G2Affine::from(G2Affine::generator() * b); - let p = pairing(&g, &h); - - assert!(p != Gt::identity()); - - let expected = G1Affine::from(G1Affine::generator() * c); - - assert_eq!(p, pairing(&expected, &G2Affine::generator())); - assert_eq!( - p, - pairing(&G1Affine::generator(), &G2Affine::generator()) * c - ); -} - -#[test] -fn test_unitary() { - let g = G1Affine::generator(); - let h = G2Affine::generator(); - let p = -pairing(&g, &h); - let q = pairing(&g, &-h); - let r = pairing(&-g, &h); - - assert_eq!(p, q); - assert_eq!(q, r); -} - -#[cfg(feature = "alloc")] -#[test] -fn test_multi_miller_loop() { - let a1 = G1Affine::generator(); - let b1 = G2Affine::generator(); - - let a2 = G1Affine::from( - G1Affine::generator() * Scalar::from_raw([1, 2, 3, 4]).invert().unwrap().square(), - ); - let b2 = G2Affine::from( - G2Affine::generator() * Scalar::from_raw([4, 2, 2, 4]).invert().unwrap().square(), - ); - - let a3 = G1Affine::identity(); - let b3 = G2Affine::from( - G2Affine::generator() * Scalar::from_raw([9, 2, 2, 4]).invert().unwrap().square(), - ); - - let a4 = G1Affine::from( - G1Affine::generator() * Scalar::from_raw([5, 5, 5, 5]).invert().unwrap().square(), - ); - let b4 = G2Affine::identity(); - - let a5 = G1Affine::from( - G1Affine::generator() * Scalar::from_raw([323, 32, 3, 1]).invert().unwrap().square(), - ); - let b5 = G2Affine::from( - G2Affine::generator() * Scalar::from_raw([4, 2, 2, 9099]).invert().unwrap().square(), - ); - - let b1_prepared = G2Prepared::from(b1); - let b2_prepared = G2Prepared::from(b2); - let b3_prepared = G2Prepared::from(b3); - let b4_prepared = G2Prepared::from(b4); - let b5_prepared = G2Prepared::from(b5); - - let expected = pairing(&a1, &b1) - + pairing(&a2, &b2) - + pairing(&a3, &b3) - + pairing(&a4, &b4) - + pairing(&a5, &b5); - - let test = multi_miller_loop(&[ - (&a1, &b1_prepared), - (&a2, &b2_prepared), - (&a3, &b3_prepared), - (&a4, &b4_prepared), - (&a5, &b5_prepared), - ]) - .final_exponentiation(); - - assert_eq!(expected, test); -} - -#[test] -fn test_miller_loop_result_default() { - assert_eq!( - MillerLoopResult::default().final_exponentiation(), - Gt::identity(), - ); -} - -#[cfg(feature = "zeroize")] -#[test] -fn test_miller_loop_result_zeroize() { - use zeroize::Zeroize; - - let mut m = multi_miller_loop(&[ - (&G1Affine::generator(), &G2Affine::generator().into()), - (&-G1Affine::generator(), &G2Affine::generator().into()), - ]); - m.zeroize(); - assert_eq!(m.0, MillerLoopResult::default().0); -} - -#[test] -fn tricking_miller_loop_result() { - assert_eq!( - multi_miller_loop(&[(&G1Affine::identity(), &G2Affine::generator().into())]).0, - Fp12::one() - ); - assert_eq!( - multi_miller_loop(&[(&G1Affine::generator(), &G2Affine::identity().into())]).0, - Fp12::one() - ); - assert_ne!( - multi_miller_loop(&[ - (&G1Affine::generator(), &G2Affine::generator().into()), - (&-G1Affine::generator(), &G2Affine::generator().into()) - ]) - .0, - Fp12::one() - ); - assert_eq!( - multi_miller_loop(&[ - (&G1Affine::generator(), &G2Affine::generator().into()), - (&-G1Affine::generator(), &G2Affine::generator().into()) - ]) - .final_exponentiation(), - Gt::identity() - ); -} diff --git a/constantine/bls12_381/src/scalar.rs b/constantine/bls12_381/src/scalar.rs deleted file mode 100644 index c253ed427..000000000 --- a/constantine/bls12_381/src/scalar.rs +++ /dev/null @@ -1,1278 +0,0 @@ -//! This module provides an implementation of the BLS12-381 scalar field $\mathbb{F}_q$ -//! where `q = 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001` -#![allow(clippy::all)] - -use core::fmt; -use core::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}; -use rand_core::RngCore; - -use ff::{Field, PrimeField}; -use subtle::{Choice, ConditionallySelectable, ConstantTimeEq, CtOption}; - -#[cfg(feature = "bits")] -use ff::{FieldBits, PrimeFieldBits}; - -use crate::util::{adc, mac, sbb}; - -/// Represents an element of the scalar field $\mathbb{F}_q$ of the BLS12-381 elliptic -/// curve construction. -// The internal representation of this type is four 64-bit unsigned -// integers in little-endian order. `Scalar` values are always in -// Montgomery form; i.e., Scalar(a) = aR mod q, with R = 2^256. -#[derive(Clone, Copy, Eq)] -pub struct Scalar(pub [u64; 4]); - -impl fmt::Debug for Scalar { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let tmp = self.to_bytes(); - write!(f, "0x")?; - for &b in tmp.iter().rev() { - write!(f, "{:02x}", b)?; - } - Ok(()) - } -} - -impl fmt::Display for Scalar { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{:?}", self) - } -} - -impl From for Scalar { - fn from(val: u64) -> Scalar { - Scalar([val, 0, 0, 0]) * R2 - } -} - -impl ConstantTimeEq for Scalar { - fn ct_eq(&self, other: &Self) -> Choice { - self.0[0].ct_eq(&other.0[0]) - & self.0[1].ct_eq(&other.0[1]) - & self.0[2].ct_eq(&other.0[2]) - & self.0[3].ct_eq(&other.0[3]) - } -} - -impl PartialEq for Scalar { - #[inline] - fn eq(&self, other: &Self) -> bool { - bool::from(self.ct_eq(other)) - } -} - -impl ConditionallySelectable for Scalar { - fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self { - Scalar([ - u64::conditional_select(&a.0[0], &b.0[0], choice), - u64::conditional_select(&a.0[1], &b.0[1], choice), - u64::conditional_select(&a.0[2], &b.0[2], choice), - u64::conditional_select(&a.0[3], &b.0[3], choice), - ]) - } -} - -/// Constant representing the modulus -/// q = 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001 -pub const MODULUS: Scalar = Scalar([ - 0xffff_ffff_0000_0001, - 0x53bd_a402_fffe_5bfe, - 0x3339_d808_09a1_d805, - 0x73ed_a753_299d_7d48, -]); - -/// The modulus as u32 limbs. -#[cfg(all(feature = "bits", not(target_pointer_width = "64")))] -const MODULUS_LIMBS_32: [u32; 8] = [ - 0x0000_0001, - 0xffff_ffff, - 0xfffe_5bfe, - 0x53bd_a402, - 0x09a1_d805, - 0x3339_d808, - 0x299d_7d48, - 0x73ed_a753, -]; - -// The number of bits needed to represent the modulus. -const MODULUS_BITS: u32 = 255; - -// GENERATOR = 7 (multiplicative generator of r-1 order, that is also quadratic nonresidue) -const GENERATOR: Scalar = Scalar([ - 0x0000_000e_ffff_fff1, - 0x17e3_63d3_0018_9c0f, - 0xff9c_5787_6f84_57b0, - 0x3513_3220_8fc5_a8c4, -]); - -impl<'a> Neg for &'a Scalar { - type Output = Scalar; - - #[inline] - fn neg(self) -> Scalar { - self.neg() - } -} - -impl Neg for Scalar { - type Output = Scalar; - - #[inline] - fn neg(self) -> Scalar { - -&self - } -} - -impl<'a, 'b> Sub<&'b Scalar> for &'a Scalar { - type Output = Scalar; - - #[inline] - fn sub(self, rhs: &'b Scalar) -> Scalar { - self.sub(rhs) - } -} - -impl<'a, 'b> Add<&'b Scalar> for &'a Scalar { - type Output = Scalar; - - #[inline] - fn add(self, rhs: &'b Scalar) -> Scalar { - self.add(rhs) - } -} - -impl<'a, 'b> Mul<&'b Scalar> for &'a Scalar { - type Output = Scalar; - - #[inline] - fn mul(self, rhs: &'b Scalar) -> Scalar { - self.mul(rhs) - } -} - -impl_binops_additive!(Scalar, Scalar); -impl_binops_multiplicative!(Scalar, Scalar); - -/// INV = -(q^{-1} mod 2^64) mod 2^64 -const INV: u64 = 0xffff_fffe_ffff_ffff; - -/// R = 2^256 mod q -const R: Scalar = Scalar([ - 0x0000_0001_ffff_fffe, - 0x5884_b7fa_0003_4802, - 0x998c_4fef_ecbc_4ff5, - 0x1824_b159_acc5_056f, -]); - -/// R^2 = 2^512 mod q -pub const R2: Scalar = Scalar([ - 0xc999_e990_f3f2_9c6d, - 0x2b6c_edcb_8792_5c23, - 0x05d3_1496_7254_398f, - 0x0748_d9d9_9f59_ff11, -]); - -/// R^3 = 2^768 mod q -const R3: Scalar = Scalar([ - 0xc62c_1807_439b_73af, - 0x1b3e_0d18_8cf0_6990, - 0x73d1_3c71_c7b5_f418, - 0x6e2a_5bb9_c8db_33e9, -]); - -/// 2^-1 -const TWO_INV: Scalar = Scalar([ - 0x0000_0000_ffff_ffff, - 0xac42_5bfd_0001_a401, - 0xccc6_27f7_f65e_27fa, - 0x0c12_58ac_d662_82b7, -]); - -// 2^S * t = MODULUS - 1 with t odd -const S: u32 = 32; - -/// GENERATOR^t where t * 2^s + 1 = q -/// with t odd. In other words, this -/// is a 2^s root of unity. -/// -/// `GENERATOR = 7 mod q` is a generator -/// of the q - 1 order multiplicative -/// subgroup. -const ROOT_OF_UNITY: Scalar = Scalar([ - 0xb9b5_8d8c_5f0e_466a, - 0x5b1b_4c80_1819_d7ec, - 0x0af5_3ae3_52a3_1e64, - 0x5bf3_adda_19e9_b27b, -]); - -/// ROOT_OF_UNITY^-1 -const ROOT_OF_UNITY_INV: Scalar = Scalar([ - 0x4256_481a_dcf3_219a, - 0x45f3_7b7f_96b6_cad3, - 0xf9c3_f1d7_5f7a_3b27, - 0x2d2f_c049_658a_fd43, -]); - -/// GENERATOR^{2^s} where t * 2^s + 1 = q with t odd. -/// In other words, this is a t root of unity. -const DELTA: Scalar = Scalar([ - 0x70e3_10d3_d146_f96a, - 0x4b64_c089_19e2_99e6, - 0x51e1_1418_6a8b_970d, - 0x6185_d066_27c0_67cb, -]); - -impl Default for Scalar { - #[inline] - fn default() -> Self { - Self::zero() - } -} - -#[cfg(feature = "zeroize")] -impl zeroize::DefaultIsZeroes for Scalar {} - -impl Scalar { - /// Returns zero, the additive identity. - #[inline] - pub const fn zero() -> Scalar { - Scalar([0, 0, 0, 0]) - } - - /// Returns one, the multiplicative identity. - #[inline] - pub const fn one() -> Scalar { - R - } - - /// Doubles this field element. - #[inline] - pub const fn double(&self) -> Scalar { - // TODO: This can be achieved more efficiently with a bitshift. - self.add(self) - } - - /// Attempts to convert a little-endian byte representation of - /// a scalar into a `Scalar`, failing if the input is not canonical. - pub fn from_bytes(bytes: &[u8; 32]) -> CtOption { - let mut tmp = Scalar([0, 0, 0, 0]); - - tmp.0[0] = u64::from_le_bytes(<[u8; 8]>::try_from(&bytes[0..8]).unwrap()); - tmp.0[1] = u64::from_le_bytes(<[u8; 8]>::try_from(&bytes[8..16]).unwrap()); - tmp.0[2] = u64::from_le_bytes(<[u8; 8]>::try_from(&bytes[16..24]).unwrap()); - tmp.0[3] = u64::from_le_bytes(<[u8; 8]>::try_from(&bytes[24..32]).unwrap()); - - // Try to subtract the modulus - let (_, borrow) = sbb(tmp.0[0], MODULUS.0[0], 0); - let (_, borrow) = sbb(tmp.0[1], MODULUS.0[1], borrow); - let (_, borrow) = sbb(tmp.0[2], MODULUS.0[2], borrow); - let (_, borrow) = sbb(tmp.0[3], MODULUS.0[3], borrow); - - // If the element is smaller than MODULUS then the - // subtraction will underflow, producing a borrow value - // of 0xffff...ffff. Otherwise, it'll be zero. - let is_some = (borrow as u8) & 1; - - // Convert to Montgomery form by computing - // (a.R^0 * R^2) / R = a.R - tmp *= &R2; - - CtOption::new(tmp, Choice::from(is_some)) - } - - /// Converts an element of `Scalar` into a byte representation in - /// little-endian byte order. - pub fn to_bytes(&self) -> [u8; 32] { - // Turn into canonical form by computing - // (a.R) / R = a - let tmp = Scalar::montgomery_reduce(self.0[0], self.0[1], self.0[2], self.0[3], 0, 0, 0, 0); - - let mut res = [0; 32]; - res[0..8].copy_from_slice(&tmp.0[0].to_le_bytes()); - res[8..16].copy_from_slice(&tmp.0[1].to_le_bytes()); - res[16..24].copy_from_slice(&tmp.0[2].to_le_bytes()); - res[24..32].copy_from_slice(&tmp.0[3].to_le_bytes()); - - res - } - - /// Converts a 512-bit little endian integer into - /// a `Scalar` by reducing by the modulus. - pub fn from_bytes_wide(bytes: &[u8; 64]) -> Scalar { - Scalar::from_u512([ - u64::from_le_bytes(<[u8; 8]>::try_from(&bytes[0..8]).unwrap()), - u64::from_le_bytes(<[u8; 8]>::try_from(&bytes[8..16]).unwrap()), - u64::from_le_bytes(<[u8; 8]>::try_from(&bytes[16..24]).unwrap()), - u64::from_le_bytes(<[u8; 8]>::try_from(&bytes[24..32]).unwrap()), - u64::from_le_bytes(<[u8; 8]>::try_from(&bytes[32..40]).unwrap()), - u64::from_le_bytes(<[u8; 8]>::try_from(&bytes[40..48]).unwrap()), - u64::from_le_bytes(<[u8; 8]>::try_from(&bytes[48..56]).unwrap()), - u64::from_le_bytes(<[u8; 8]>::try_from(&bytes[56..64]).unwrap()), - ]) - } - - fn from_u512(limbs: [u64; 8]) -> Scalar { - // We reduce an arbitrary 512-bit number by decomposing it into two 256-bit digits - // with the higher bits multiplied by 2^256. Thus, we perform two reductions - // - // 1. the lower bits are multiplied by R^2, as normal - // 2. the upper bits are multiplied by R^2 * 2^256 = R^3 - // - // and computing their sum in the field. It remains to see that arbitrary 256-bit - // numbers can be placed into Montgomery form safely using the reduction. The - // reduction works so long as the product is less than R=2^256 multiplied by - // the modulus. This holds because for any `c` smaller than the modulus, we have - // that (2^256 - 1)*c is an acceptable product for the reduction. Therefore, the - // reduction always works so long as `c` is in the field; in this case it is either the - // constant `R2` or `R3`. - let d0 = Scalar([limbs[0], limbs[1], limbs[2], limbs[3]]); - let d1 = Scalar([limbs[4], limbs[5], limbs[6], limbs[7]]); - // Convert to Montgomery form - d0 * R2 + d1 * R3 - } - - /// Converts from an integer represented in little endian - /// into its (congruent) `Scalar` representation. - pub const fn from_raw(val: [u64; 4]) -> Self { - (&Scalar(val)).mul(&R2) - } - - /// Squares this element. - #[inline] - pub const fn square(&self) -> Scalar { - let (r1, carry) = mac(0, self.0[0], self.0[1], 0); - let (r2, carry) = mac(0, self.0[0], self.0[2], carry); - let (r3, r4) = mac(0, self.0[0], self.0[3], carry); - - let (r3, carry) = mac(r3, self.0[1], self.0[2], 0); - let (r4, r5) = mac(r4, self.0[1], self.0[3], carry); - - let (r5, r6) = mac(r5, self.0[2], self.0[3], 0); - - let r7 = r6 >> 63; - let r6 = (r6 << 1) | (r5 >> 63); - let r5 = (r5 << 1) | (r4 >> 63); - let r4 = (r4 << 1) | (r3 >> 63); - let r3 = (r3 << 1) | (r2 >> 63); - let r2 = (r2 << 1) | (r1 >> 63); - let r1 = r1 << 1; - - let (r0, carry) = mac(0, self.0[0], self.0[0], 0); - let (r1, carry) = adc(0, r1, carry); - let (r2, carry) = mac(r2, self.0[1], self.0[1], carry); - let (r3, carry) = adc(0, r3, carry); - let (r4, carry) = mac(r4, self.0[2], self.0[2], carry); - let (r5, carry) = adc(0, r5, carry); - let (r6, carry) = mac(r6, self.0[3], self.0[3], carry); - let (r7, _) = adc(0, r7, carry); - - Scalar::montgomery_reduce(r0, r1, r2, r3, r4, r5, r6, r7) - } - - /// Exponentiates `self` by `by`, where `by` is a - /// little-endian order integer exponent. - pub fn pow(&self, by: &[u64; 4]) -> Self { - let mut res = Self::one(); - for e in by.iter().rev() { - for i in (0..64).rev() { - res = res.square(); - let mut tmp = res; - tmp *= self; - res.conditional_assign(&tmp, (((*e >> i) & 0x1) as u8).into()); - } - } - res - } - - /// Exponentiates `self` by `by`, where `by` is a - /// little-endian order integer exponent. - /// - /// **This operation is variable time with respect - /// to the exponent.** If the exponent is fixed, - /// this operation is effectively constant time. - pub fn pow_vartime(&self, by: &[u64; 4]) -> Self { - let mut res = Self::one(); - for e in by.iter().rev() { - for i in (0..64).rev() { - res = res.square(); - - if ((*e >> i) & 1) == 1 { - res.mul_assign(self); - } - } - } - res - } - - /// Computes the multiplicative inverse of this element, - /// failing if the element is zero. - pub fn invert(&self) -> CtOption { - #[inline(always)] - fn square_assign_multi(n: &mut Scalar, num_times: usize) { - for _ in 0..num_times { - *n = n.square(); - } - } - // found using https://github.com/kwantam/addchain - let mut t0 = self.square(); - let mut t1 = t0 * self; - let mut t16 = t0.square(); - let mut t6 = t16.square(); - let mut t5 = t6 * t0; - t0 = t6 * t16; - let mut t12 = t5 * t16; - let mut t2 = t6.square(); - let mut t7 = t5 * t6; - let mut t15 = t0 * t5; - let mut t17 = t12.square(); - t1 *= t17; - let mut t3 = t7 * t2; - let t8 = t1 * t17; - let t4 = t8 * t2; - let t9 = t8 * t7; - t7 = t4 * t5; - let t11 = t4 * t17; - t5 = t9 * t17; - let t14 = t7 * t15; - let t13 = t11 * t12; - t12 = t11 * t17; - t15 *= &t12; - t16 *= &t15; - t3 *= &t16; - t17 *= &t3; - t0 *= &t17; - t6 *= &t0; - t2 *= &t6; - square_assign_multi(&mut t0, 8); - t0 *= &t17; - square_assign_multi(&mut t0, 9); - t0 *= &t16; - square_assign_multi(&mut t0, 9); - t0 *= &t15; - square_assign_multi(&mut t0, 9); - t0 *= &t15; - square_assign_multi(&mut t0, 7); - t0 *= &t14; - square_assign_multi(&mut t0, 7); - t0 *= &t13; - square_assign_multi(&mut t0, 10); - t0 *= &t12; - square_assign_multi(&mut t0, 9); - t0 *= &t11; - square_assign_multi(&mut t0, 8); - t0 *= &t8; - square_assign_multi(&mut t0, 8); - t0 *= self; - square_assign_multi(&mut t0, 14); - t0 *= &t9; - square_assign_multi(&mut t0, 10); - t0 *= &t8; - square_assign_multi(&mut t0, 15); - t0 *= &t7; - square_assign_multi(&mut t0, 10); - t0 *= &t6; - square_assign_multi(&mut t0, 8); - t0 *= &t5; - square_assign_multi(&mut t0, 16); - t0 *= &t3; - square_assign_multi(&mut t0, 8); - t0 *= &t2; - square_assign_multi(&mut t0, 7); - t0 *= &t4; - square_assign_multi(&mut t0, 9); - t0 *= &t2; - square_assign_multi(&mut t0, 8); - t0 *= &t3; - square_assign_multi(&mut t0, 8); - t0 *= &t2; - square_assign_multi(&mut t0, 8); - t0 *= &t2; - square_assign_multi(&mut t0, 8); - t0 *= &t2; - square_assign_multi(&mut t0, 8); - t0 *= &t3; - square_assign_multi(&mut t0, 8); - t0 *= &t2; - square_assign_multi(&mut t0, 8); - t0 *= &t2; - square_assign_multi(&mut t0, 5); - t0 *= &t1; - square_assign_multi(&mut t0, 5); - t0 *= &t1; - - CtOption::new(t0, !self.ct_eq(&Self::zero())) - } - - #[inline(always)] - pub const fn montgomery_reduce( - r0: u64, - r1: u64, - r2: u64, - r3: u64, - r4: u64, - r5: u64, - r6: u64, - r7: u64, - ) -> Self { - // The Montgomery reduction here is based on Algorithm 14.32 in - // Handbook of Applied Cryptography - // . - - let k = r0.wrapping_mul(INV); - let (_, carry) = mac(r0, k, MODULUS.0[0], 0); - let (r1, carry) = mac(r1, k, MODULUS.0[1], carry); - let (r2, carry) = mac(r2, k, MODULUS.0[2], carry); - let (r3, carry) = mac(r3, k, MODULUS.0[3], carry); - let (r4, carry2) = adc(r4, 0, carry); - - let k = r1.wrapping_mul(INV); - let (_, carry) = mac(r1, k, MODULUS.0[0], 0); - let (r2, carry) = mac(r2, k, MODULUS.0[1], carry); - let (r3, carry) = mac(r3, k, MODULUS.0[2], carry); - let (r4, carry) = mac(r4, k, MODULUS.0[3], carry); - let (r5, carry2) = adc(r5, carry2, carry); - - let k = r2.wrapping_mul(INV); - let (_, carry) = mac(r2, k, MODULUS.0[0], 0); - let (r3, carry) = mac(r3, k, MODULUS.0[1], carry); - let (r4, carry) = mac(r4, k, MODULUS.0[2], carry); - let (r5, carry) = mac(r5, k, MODULUS.0[3], carry); - let (r6, carry2) = adc(r6, carry2, carry); - - let k = r3.wrapping_mul(INV); - let (_, carry) = mac(r3, k, MODULUS.0[0], 0); - let (r4, carry) = mac(r4, k, MODULUS.0[1], carry); - let (r5, carry) = mac(r5, k, MODULUS.0[2], carry); - let (r6, carry) = mac(r6, k, MODULUS.0[3], carry); - let (r7, _) = adc(r7, carry2, carry); - - // Result may be within MODULUS of the correct value - (&Scalar([r4, r5, r6, r7])).sub(&MODULUS) - } - - /// Multiplies `rhs` by `self`, returning the result. - #[inline] - pub const fn mul(&self, rhs: &Self) -> Self { - // Schoolbook multiplication - - let (r0, carry) = mac(0, self.0[0], rhs.0[0], 0); - let (r1, carry) = mac(0, self.0[0], rhs.0[1], carry); - let (r2, carry) = mac(0, self.0[0], rhs.0[2], carry); - let (r3, r4) = mac(0, self.0[0], rhs.0[3], carry); - - let (r1, carry) = mac(r1, self.0[1], rhs.0[0], 0); - let (r2, carry) = mac(r2, self.0[1], rhs.0[1], carry); - let (r3, carry) = mac(r3, self.0[1], rhs.0[2], carry); - let (r4, r5) = mac(r4, self.0[1], rhs.0[3], carry); - - let (r2, carry) = mac(r2, self.0[2], rhs.0[0], 0); - let (r3, carry) = mac(r3, self.0[2], rhs.0[1], carry); - let (r4, carry) = mac(r4, self.0[2], rhs.0[2], carry); - let (r5, r6) = mac(r5, self.0[2], rhs.0[3], carry); - - let (r3, carry) = mac(r3, self.0[3], rhs.0[0], 0); - let (r4, carry) = mac(r4, self.0[3], rhs.0[1], carry); - let (r5, carry) = mac(r5, self.0[3], rhs.0[2], carry); - let (r6, r7) = mac(r6, self.0[3], rhs.0[3], carry); - - Scalar::montgomery_reduce(r0, r1, r2, r3, r4, r5, r6, r7) - } - - /// Subtracts `rhs` from `self`, returning the result. - #[inline] - pub const fn sub(&self, rhs: &Self) -> Self { - let (d0, borrow) = sbb(self.0[0], rhs.0[0], 0); - let (d1, borrow) = sbb(self.0[1], rhs.0[1], borrow); - let (d2, borrow) = sbb(self.0[2], rhs.0[2], borrow); - let (d3, borrow) = sbb(self.0[3], rhs.0[3], borrow); - - // If underflow occurred on the final limb, borrow = 0xfff...fff, otherwise - // borrow = 0x000...000. Thus, we use it as a mask to conditionally add the modulus. - let (d0, carry) = adc(d0, MODULUS.0[0] & borrow, 0); - let (d1, carry) = adc(d1, MODULUS.0[1] & borrow, carry); - let (d2, carry) = adc(d2, MODULUS.0[2] & borrow, carry); - let (d3, _) = adc(d3, MODULUS.0[3] & borrow, carry); - - Scalar([d0, d1, d2, d3]) - } - - /// Adds `rhs` to `self`, returning the result. - #[inline] - pub const fn add(&self, rhs: &Self) -> Self { - let (d0, carry) = adc(self.0[0], rhs.0[0], 0); - let (d1, carry) = adc(self.0[1], rhs.0[1], carry); - let (d2, carry) = adc(self.0[2], rhs.0[2], carry); - let (d3, _) = adc(self.0[3], rhs.0[3], carry); - - // Attempt to subtract the modulus, to ensure the value - // is smaller than the modulus. - (&Scalar([d0, d1, d2, d3])).sub(&MODULUS) - } - - /// Negates `self`. - #[inline] - pub const fn neg(&self) -> Self { - // Subtract `self` from `MODULUS` to negate. Ignore the final - // borrow because it cannot underflow; self is guaranteed to - // be in the field. - let (d0, borrow) = sbb(MODULUS.0[0], self.0[0], 0); - let (d1, borrow) = sbb(MODULUS.0[1], self.0[1], borrow); - let (d2, borrow) = sbb(MODULUS.0[2], self.0[2], borrow); - let (d3, _) = sbb(MODULUS.0[3], self.0[3], borrow); - - // `tmp` could be `MODULUS` if `self` was zero. Create a mask that is - // zero if `self` was zero, and `u64::max_value()` if self was nonzero. - let mask = (((self.0[0] | self.0[1] | self.0[2] | self.0[3]) == 0) as u64).wrapping_sub(1); - - Scalar([d0 & mask, d1 & mask, d2 & mask, d3 & mask]) - } -} - -impl From for [u8; 32] { - fn from(value: Scalar) -> [u8; 32] { - value.to_bytes() - } -} - -impl<'a> From<&'a Scalar> for [u8; 32] { - fn from(value: &'a Scalar) -> [u8; 32] { - value.to_bytes() - } -} - -impl Field for Scalar { - const ZERO: Self = Self::zero(); - const ONE: Self = Self::one(); - - fn random(mut rng: impl RngCore) -> Self { - let mut buf = [0; 64]; - rng.fill_bytes(&mut buf); - Self::from_bytes_wide(&buf) - } - - #[must_use] - fn square(&self) -> Self { - self.square() - } - - #[must_use] - fn double(&self) -> Self { - self.double() - } - - fn invert(&self) -> CtOption { - self.invert() - } - - fn sqrt_ratio(num: &Self, div: &Self) -> (Choice, Self) { - ff::helpers::sqrt_ratio_generic(num, div) - } - - fn sqrt(&self) -> CtOption { - // (t - 1) // 2 = 6104339283789297388802252303364915521546564123189034618274734669823 - ff::helpers::sqrt_tonelli_shanks( - self, - &[ - 0x7fff_2dff_7fff_ffff, - 0x04d0_ec02_a9de_d201, - 0x94ce_bea4_199c_ec04, - 0x0000_0000_39f6_d3a9, - ], - ) - } - - fn is_zero_vartime(&self) -> bool { - self.0 == Self::zero().0 - } -} - -impl PrimeField for Scalar { - type Repr = [u8; 32]; - - fn from_repr(r: Self::Repr) -> CtOption { - Self::from_bytes(&r) - } - - fn to_repr(&self) -> Self::Repr { - self.to_bytes() - } - - fn is_odd(&self) -> Choice { - Choice::from(self.to_bytes()[0] & 1) - } - - const MODULUS: &'static str = - "0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001"; - const NUM_BITS: u32 = MODULUS_BITS; - const CAPACITY: u32 = Self::NUM_BITS - 1; - const TWO_INV: Self = TWO_INV; - const MULTIPLICATIVE_GENERATOR: Self = GENERATOR; - const S: u32 = S; - const ROOT_OF_UNITY: Self = ROOT_OF_UNITY; - const ROOT_OF_UNITY_INV: Self = ROOT_OF_UNITY_INV; - const DELTA: Self = DELTA; -} - -#[cfg(all(feature = "bits", not(target_pointer_width = "64")))] -type ReprBits = [u32; 8]; - -#[cfg(all(feature = "bits", target_pointer_width = "64"))] -type ReprBits = [u64; 4]; - -#[cfg(feature = "bits")] -impl PrimeFieldBits for Scalar { - type ReprBits = ReprBits; - - fn to_le_bits(&self) -> FieldBits { - let bytes = self.to_bytes(); - - #[cfg(not(target_pointer_width = "64"))] - let limbs = [ - u32::from_le_bytes(bytes[0..4].try_into().unwrap()), - u32::from_le_bytes(bytes[4..8].try_into().unwrap()), - u32::from_le_bytes(bytes[8..12].try_into().unwrap()), - u32::from_le_bytes(bytes[12..16].try_into().unwrap()), - u32::from_le_bytes(bytes[16..20].try_into().unwrap()), - u32::from_le_bytes(bytes[20..24].try_into().unwrap()), - u32::from_le_bytes(bytes[24..28].try_into().unwrap()), - u32::from_le_bytes(bytes[28..32].try_into().unwrap()), - ]; - - #[cfg(target_pointer_width = "64")] - let limbs = [ - u64::from_le_bytes(bytes[0..8].try_into().unwrap()), - u64::from_le_bytes(bytes[8..16].try_into().unwrap()), - u64::from_le_bytes(bytes[16..24].try_into().unwrap()), - u64::from_le_bytes(bytes[24..32].try_into().unwrap()), - ]; - - FieldBits::new(limbs) - } - - fn char_le_bits() -> FieldBits { - #[cfg(not(target_pointer_width = "64"))] - { - FieldBits::new(MODULUS_LIMBS_32) - } - - #[cfg(target_pointer_width = "64")] - FieldBits::new(MODULUS.0) - } -} - -impl core::iter::Sum for Scalar -where - T: core::borrow::Borrow, -{ - fn sum(iter: I) -> Self - where - I: Iterator, - { - iter.fold(Self::zero(), |acc, item| acc + item.borrow()) - } -} - -impl core::iter::Product for Scalar -where - T: core::borrow::Borrow, -{ - fn product(iter: I) -> Self - where - I: Iterator, - { - iter.fold(Self::one(), |acc, item| acc * item.borrow()) - } -} - -#[test] -fn test_constants() { - assert_eq!( - Scalar::MODULUS, - "0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001", - ); - - assert_eq!(Scalar::from(2) * Scalar::TWO_INV, Scalar::ONE); - - assert_eq!( - Scalar::ROOT_OF_UNITY * Scalar::ROOT_OF_UNITY_INV, - Scalar::ONE, - ); - - // ROOT_OF_UNITY^{2^s} mod m == 1 - assert_eq!( - Scalar::ROOT_OF_UNITY.pow(&[1u64 << Scalar::S, 0, 0, 0]), - Scalar::ONE, - ); - - // DELTA^{t} mod m == 1 - assert_eq!( - Scalar::DELTA.pow(&[ - 0xfffe_5bfe_ffff_ffff, - 0x09a1_d805_53bd_a402, - 0x299d_7d48_3339_d808, - 0x0000_0000_73ed_a753, - ]), - Scalar::ONE, - ); -} - -#[test] -fn test_inv() { - // Compute -(q^{-1} mod 2^64) mod 2^64 by exponentiating - // by totient(2**64) - 1 - - let mut inv = 1u64; - for _ in 0..63 { - inv = inv.wrapping_mul(inv); - inv = inv.wrapping_mul(MODULUS.0[0]); - } - inv = inv.wrapping_neg(); - - assert_eq!(inv, INV); -} - -#[cfg(feature = "std")] -#[test] -fn test_debug() { - assert_eq!( - format!("{:?}", Scalar::zero()), - "0x0000000000000000000000000000000000000000000000000000000000000000" - ); - assert_eq!( - format!("{:?}", Scalar::one()), - "0x0000000000000000000000000000000000000000000000000000000000000001" - ); - assert_eq!( - format!("{:?}", R2), - "0x1824b159acc5056f998c4fefecbc4ff55884b7fa0003480200000001fffffffe" - ); -} - -#[test] -fn test_equality() { - assert_eq!(Scalar::zero(), Scalar::zero()); - assert_eq!(Scalar::one(), Scalar::one()); - assert_eq!(R2, R2); - - assert!(Scalar::zero() != Scalar::one()); - assert!(Scalar::one() != R2); -} - -#[test] -fn test_to_bytes() { - assert_eq!( - Scalar::zero().to_bytes(), - [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0 - ] - ); - - assert_eq!( - Scalar::one().to_bytes(), - [ - 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0 - ] - ); - - assert_eq!( - R2.to_bytes(), - [ - 254, 255, 255, 255, 1, 0, 0, 0, 2, 72, 3, 0, 250, 183, 132, 88, 245, 79, 188, 236, 239, - 79, 140, 153, 111, 5, 197, 172, 89, 177, 36, 24 - ] - ); - - assert_eq!( - (-&Scalar::one()).to_bytes(), - [ - 0, 0, 0, 0, 255, 255, 255, 255, 254, 91, 254, 255, 2, 164, 189, 83, 5, 216, 161, 9, 8, - 216, 57, 51, 72, 125, 157, 41, 83, 167, 237, 115 - ] - ); -} - -#[test] -fn test_from_bytes() { - assert_eq!( - Scalar::from_bytes(&[ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0 - ]) - .unwrap(), - Scalar::zero() - ); - - assert_eq!( - Scalar::from_bytes(&[ - 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0 - ]) - .unwrap(), - Scalar::one() - ); - - assert_eq!( - Scalar::from_bytes(&[ - 254, 255, 255, 255, 1, 0, 0, 0, 2, 72, 3, 0, 250, 183, 132, 88, 245, 79, 188, 236, 239, - 79, 140, 153, 111, 5, 197, 172, 89, 177, 36, 24 - ]) - .unwrap(), - R2 - ); - - // -1 should work - assert!(bool::from( - Scalar::from_bytes(&[ - 0, 0, 0, 0, 255, 255, 255, 255, 254, 91, 254, 255, 2, 164, 189, 83, 5, 216, 161, 9, 8, - 216, 57, 51, 72, 125, 157, 41, 83, 167, 237, 115 - ]) - .is_some() - )); - - // modulus is invalid - assert!(bool::from( - Scalar::from_bytes(&[ - 1, 0, 0, 0, 255, 255, 255, 255, 254, 91, 254, 255, 2, 164, 189, 83, 5, 216, 161, 9, 8, - 216, 57, 51, 72, 125, 157, 41, 83, 167, 237, 115 - ]) - .is_none() - )); - - // Anything larger than the modulus is invalid - assert!(bool::from( - Scalar::from_bytes(&[ - 2, 0, 0, 0, 255, 255, 255, 255, 254, 91, 254, 255, 2, 164, 189, 83, 5, 216, 161, 9, 8, - 216, 57, 51, 72, 125, 157, 41, 83, 167, 237, 115 - ]) - .is_none() - )); - assert!(bool::from( - Scalar::from_bytes(&[ - 1, 0, 0, 0, 255, 255, 255, 255, 254, 91, 254, 255, 2, 164, 189, 83, 5, 216, 161, 9, 8, - 216, 58, 51, 72, 125, 157, 41, 83, 167, 237, 115 - ]) - .is_none() - )); - assert!(bool::from( - Scalar::from_bytes(&[ - 1, 0, 0, 0, 255, 255, 255, 255, 254, 91, 254, 255, 2, 164, 189, 83, 5, 216, 161, 9, 8, - 216, 57, 51, 72, 125, 157, 41, 83, 167, 237, 116 - ]) - .is_none() - )); -} - -#[test] -fn test_from_u512_zero() { - assert_eq!( - Scalar::zero(), - Scalar::from_u512([ - MODULUS.0[0], - MODULUS.0[1], - MODULUS.0[2], - MODULUS.0[3], - 0, - 0, - 0, - 0 - ]) - ); -} - -#[test] -fn test_from_u512_r() { - assert_eq!(R, Scalar::from_u512([1, 0, 0, 0, 0, 0, 0, 0])); -} - -#[test] -fn test_from_u512_r2() { - assert_eq!(R2, Scalar::from_u512([0, 0, 0, 0, 1, 0, 0, 0])); -} - -#[test] -fn test_from_u512_max() { - let max_u64 = 0xffff_ffff_ffff_ffff; - assert_eq!( - R3 - R, - Scalar::from_u512([max_u64, max_u64, max_u64, max_u64, max_u64, max_u64, max_u64, max_u64]) - ); -} - -#[test] -fn test_from_bytes_wide_r2() { - assert_eq!( - R2, - Scalar::from_bytes_wide(&[ - 254, 255, 255, 255, 1, 0, 0, 0, 2, 72, 3, 0, 250, 183, 132, 88, 245, 79, 188, 236, 239, - 79, 140, 153, 111, 5, 197, 172, 89, 177, 36, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - ]) - ); -} - -#[test] -fn test_from_bytes_wide_negative_one() { - assert_eq!( - -&Scalar::one(), - Scalar::from_bytes_wide(&[ - 0, 0, 0, 0, 255, 255, 255, 255, 254, 91, 254, 255, 2, 164, 189, 83, 5, 216, 161, 9, 8, - 216, 57, 51, 72, 125, 157, 41, 83, 167, 237, 115, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - ]) - ); -} - -#[test] -fn test_from_bytes_wide_maximum() { - assert_eq!( - Scalar([ - 0xc62c_1805_439b_73b1, - 0xc2b9_551e_8ced_218e, - 0xda44_ec81_daf9_a422, - 0x5605_aa60_1c16_2e79, - ]), - Scalar::from_bytes_wide(&[0xff; 64]) - ); -} - -#[test] -fn test_zero() { - assert_eq!(Scalar::zero(), -&Scalar::zero()); - assert_eq!(Scalar::zero(), Scalar::zero() + Scalar::zero()); - assert_eq!(Scalar::zero(), Scalar::zero() - Scalar::zero()); - assert_eq!(Scalar::zero(), Scalar::zero() * Scalar::zero()); -} - -#[cfg(test)] -const LARGEST: Scalar = Scalar([ - 0xffff_ffff_0000_0000, - 0x53bd_a402_fffe_5bfe, - 0x3339_d808_09a1_d805, - 0x73ed_a753_299d_7d48, -]); - -#[test] -fn test_addition() { - let mut tmp = LARGEST; - tmp += &LARGEST; - - assert_eq!( - tmp, - Scalar([ - 0xffff_fffe_ffff_ffff, - 0x53bd_a402_fffe_5bfe, - 0x3339_d808_09a1_d805, - 0x73ed_a753_299d_7d48, - ]) - ); - - let mut tmp = LARGEST; - tmp += &Scalar([1, 0, 0, 0]); - - assert_eq!(tmp, Scalar::zero()); -} - -#[test] -fn test_negation() { - let tmp = -&LARGEST; - - assert_eq!(tmp, Scalar([1, 0, 0, 0])); - - let tmp = -&Scalar::zero(); - assert_eq!(tmp, Scalar::zero()); - let tmp = -&Scalar([1, 0, 0, 0]); - assert_eq!(tmp, LARGEST); -} - -#[test] -fn test_subtraction() { - let mut tmp = LARGEST; - tmp -= &LARGEST; - - assert_eq!(tmp, Scalar::zero()); - - let mut tmp = Scalar::zero(); - tmp -= &LARGEST; - - let mut tmp2 = MODULUS; - tmp2 -= &LARGEST; - - assert_eq!(tmp, tmp2); -} - -#[test] -fn test_multiplication() { - let mut cur = LARGEST; - - for _ in 0..100 { - let mut tmp = cur; - tmp *= &cur; - - let mut tmp2 = Scalar::zero(); - for b in cur - .to_bytes() - .iter() - .rev() - .flat_map(|byte| (0..8).rev().map(move |i| ((byte >> i) & 1u8) == 1u8)) - { - let tmp3 = tmp2; - tmp2.add_assign(&tmp3); - - if b { - tmp2.add_assign(&cur); - } - } - - assert_eq!(tmp, tmp2); - - cur.add_assign(&LARGEST); - } -} - -#[test] -fn test_squaring() { - let mut cur = LARGEST; - - for _ in 0..100 { - let mut tmp = cur; - tmp = tmp.square(); - - let mut tmp2 = Scalar::zero(); - for b in cur - .to_bytes() - .iter() - .rev() - .flat_map(|byte| (0..8).rev().map(move |i| ((byte >> i) & 1u8) == 1u8)) - { - let tmp3 = tmp2; - tmp2.add_assign(&tmp3); - - if b { - tmp2.add_assign(&cur); - } - } - - assert_eq!(tmp, tmp2); - - cur.add_assign(&LARGEST); - } -} - -#[test] -fn test_inversion() { - assert!(bool::from(Scalar::zero().invert().is_none())); - assert_eq!(Scalar::one().invert().unwrap(), Scalar::one()); - assert_eq!((-&Scalar::one()).invert().unwrap(), -&Scalar::one()); - - let mut tmp = R2; - - for _ in 0..100 { - let mut tmp2 = tmp.invert().unwrap(); - tmp2.mul_assign(&tmp); - - assert_eq!(tmp2, Scalar::one()); - - tmp.add_assign(&R2); - } -} - -#[test] -fn test_invert_is_pow() { - let q_minus_2 = [ - 0xffff_fffe_ffff_ffff, - 0x53bd_a402_fffe_5bfe, - 0x3339_d808_09a1_d805, - 0x73ed_a753_299d_7d48, - ]; - - let mut r1 = R; - let mut r2 = R; - let mut r3 = R; - - for _ in 0..100 { - r1 = r1.invert().unwrap(); - r2 = r2.pow_vartime(&q_minus_2); - r3 = r3.pow(&q_minus_2); - - assert_eq!(r1, r2); - assert_eq!(r2, r3); - // Add R so we check something different next time around - r1.add_assign(&R); - r2 = r1; - r3 = r1; - } -} - -#[test] -fn test_sqrt() { - { - assert_eq!(Scalar::zero().sqrt().unwrap(), Scalar::zero()); - } - - let mut square = Scalar([ - 0x46cd_85a5_f273_077e, - 0x1d30_c47d_d68f_c735, - 0x77f6_56f6_0bec_a0eb, - 0x494a_a01b_df32_468d, - ]); - - let mut none_count = 0; - - for _ in 0..100 { - let square_root = square.sqrt(); - if bool::from(square_root.is_none()) { - none_count += 1; - } else { - assert_eq!(square_root.unwrap() * square_root.unwrap(), square); - } - square -= Scalar::one(); - } - - assert_eq!(49, none_count); -} - -#[test] -fn test_from_raw() { - assert_eq!( - Scalar::from_raw([ - 0x0001_ffff_fffd, - 0x5884_b7fa_0003_4802, - 0x998c_4fef_ecbc_4ff5, - 0x1824_b159_acc5_056f, - ]), - Scalar::from_raw([0xffff_ffff_ffff_ffff; 4]) - ); - - assert_eq!(Scalar::from_raw(MODULUS.0), Scalar::zero()); - - assert_eq!(Scalar::from_raw([1, 0, 0, 0]), R); -} - -#[test] -fn test_double() { - let a = Scalar::from_raw([ - 0x1fff_3231_233f_fffd, - 0x4884_b7fa_0003_4802, - 0x998c_4fef_ecbc_4ff3, - 0x1824_b159_acc5_0562, - ]); - - assert_eq!(a.double(), a + a); -} - -#[cfg(feature = "zeroize")] -#[test] -fn test_zeroize() { - use zeroize::Zeroize; - - let mut a = Scalar::from_raw([ - 0x1fff_3231_233f_fffd, - 0x4884_b7fa_0003_4802, - 0x998c_4fef_ecbc_4ff3, - 0x1824_b159_acc5_0562, - ]); - a.zeroize(); - assert!(bool::from(a.is_zero())); -} diff --git a/constantine/bls12_381/src/tests/g1_compressed_valid_test_vectors.dat b/constantine/bls12_381/src/tests/g1_compressed_valid_test_vectors.dat deleted file mode 100644 index ea8cd67652d133010e79df8488452450b6f5cb17..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 48000 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 Rm?nepfGSPSF*XppvfIk?3d#Tg diff --git a/constantine/bls12_381/src/tests/g1_uncompressed_valid_test_vectors.dat b/constantine/bls12_381/src/tests/g1_uncompressed_valid_test_vectors.dat deleted file mode 100644 index 86abfba945c7b701750d637bffe6701330e10e88..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 96000 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%0A9l>2- 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 diff --git a/constantine/bls12_381/src/tests/g2_compressed_valid_test_vectors.dat b/constantine/bls12_381/src/tests/g2_compressed_valid_test_vectors.dat deleted file mode 100644 index a40bbe251d90e576060adb7eaa2456197878804c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 96000 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