From 37de4bf2f7e26e6d8988fcae2d6025f891cfdee6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Tue, 12 Apr 2022 11:59:26 +0100 Subject: [PATCH] Crypto part of the Groth's NIDKG (#1182) * Work in progress NIDKG * Encryption of multiple shares * Extracted baby-step giant-step lookup table as a separate entity * Proof of discrete log * Adjusted discrete log domainn * Producing proof of log during keygen * Zeroize for epoch * Proof of secret sharing * empty main for compiler appeasement * Construction of proof of chunking * Initial untested verification of proof of chunking * Converted chunk responses from Scalar to u64 * Additional tests for proof of chunking * Minor cleanup and reorganisation * Fixed enc/dec to use f0 * Deriving node coverage of required tree nodes * Finally seemingnly working encryption under nonzero epoch * Branch park * Decryption key updates to specified epochs * Ciphertext integrity checks * Progress in integration tests * Fixed ciphertext combining and integration test * Dealing type and simplification of the integration test * Benchmark for creation of baby-step-giant-step lookup table * Initial import cleanup + broken 2nd integration test * Using correct assertions in the integration test (and correctly combining shares) * Removed unused modules * Changed proof of sharing to allow for node indices being different from [1,2,...n] * Reorganised bte module * Benchmark for g2 precomputation * Created more strongly typed Epoch type which is essentially a Tau such that it is a leaf node * Extending tau with a temporary oracle output * Using random oracle for tau extension * More benchmarks! * encryption-related benchmarks * Serialization of PublicKeyWithProof * Typos * Removed any changes made in validator-api or smart contracts * Made the integration test slightly more concise * Further purge of unused modules * Fixed combining share to use lagrangian interpolation * Recovery of verification keys from the dealings * Verification key verification + extended integration tests * Fixed Tau not being included in digest for producing Tau_h * Tau serialization * Serialization of a BTE Node * Serialization of DecryptionKey * Serialization of PublicCoefficients * Utility method for setting constant coefficient of a polynomial * Serialization of Ciphertexts * Serialization of Proof of Secret Sharing * Serialization of Proof of Chunking * Serialization of Dealing * Adjusted capacity of responses_r in proof of chunking * Made notation more consistent with the paper equivalents * Optional arguments for creating/verifying resharing dealings --- Cargo.lock | 106 +- Cargo.toml | 1 + common/crypto/dkg/Cargo.toml | 41 + common/crypto/dkg/benches/benchmarks.rs | 541 +++++++++ common/crypto/dkg/src/bte/encryption.rs | 772 ++++++++++++ common/crypto/dkg/src/bte/keys.rs | 875 +++++++++++++ common/crypto/dkg/src/bte/mod.rs | 463 +++++++ common/crypto/dkg/src/bte/proof_chunking.rs | 1079 +++++++++++++++++ .../crypto/dkg/src/bte/proof_discrete_log.rs | 94 ++ common/crypto/dkg/src/bte/proof_sharing.rs | 615 ++++++++++ common/crypto/dkg/src/dealing.rs | 500 ++++++++ common/crypto/dkg/src/error.rs | 114 ++ common/crypto/dkg/src/interpolation/mod.rs | 157 +++ .../dkg/src/interpolation/polynomial.rs | 395 ++++++ common/crypto/dkg/src/lib.rs | 95 ++ common/crypto/dkg/src/share.rs | 130 ++ common/crypto/dkg/src/todos.md | 4 + common/crypto/dkg/src/utils.rs | 114 ++ common/crypto/dkg/tests/integration.rs | 285 +++++ 19 files changed, 6367 insertions(+), 14 deletions(-) create mode 100644 common/crypto/dkg/Cargo.toml create mode 100644 common/crypto/dkg/benches/benchmarks.rs create mode 100644 common/crypto/dkg/src/bte/encryption.rs create mode 100644 common/crypto/dkg/src/bte/keys.rs create mode 100644 common/crypto/dkg/src/bte/mod.rs create mode 100644 common/crypto/dkg/src/bte/proof_chunking.rs create mode 100644 common/crypto/dkg/src/bte/proof_discrete_log.rs create mode 100644 common/crypto/dkg/src/bte/proof_sharing.rs create mode 100644 common/crypto/dkg/src/dealing.rs create mode 100644 common/crypto/dkg/src/error.rs create mode 100644 common/crypto/dkg/src/interpolation/mod.rs create mode 100644 common/crypto/dkg/src/interpolation/polynomial.rs create mode 100644 common/crypto/dkg/src/lib.rs create mode 100644 common/crypto/dkg/src/share.rs create mode 100644 common/crypto/dkg/src/todos.md create mode 100644 common/crypto/dkg/src/utils.rs create mode 100644 common/crypto/dkg/tests/integration.rs diff --git a/Cargo.lock b/Cargo.lock index 8c3bc17e44..3cd2da7190 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -286,10 +286,22 @@ version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7774144344a4faa177370406a7ff5f1da24303817368584c6206c8303eb07848" dependencies = [ - "funty", - "radium", + "funty 1.1.0", + "radium 0.6.2", "tap", - "wyz", + "wyz 0.2.0", +] + +[[package]] +name = "bitvec" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1489fcb93a5bb47da0462ca93ad252ad6af2145cce58d10d46a83931ba9f016b" +dependencies = [ + "funty 2.0.0", + "radium 0.7.0", + "tap", + "wyz 0.5.0", ] [[package]] @@ -373,11 +385,25 @@ dependencies = [ "digest 0.9.0", "ff 0.10.1", "group 0.10.0", - "pairing", + "pairing 0.20.0", "rand_core 0.6.3", "subtle 2.4.1", ] +[[package]] +name = "bls12_381" +version = "0.6.0" +source = "git+https://github.com/jstuczyn/bls12_381?branch=gt-serialisation#10fb6f700bfda17c8475af3bfd31e3fec15f2278" +dependencies = [ + "digest 0.9.0", + "ff 0.11.0", + "group 0.11.0", + "pairing 0.21.0", + "rand_core 0.6.3", + "subtle 2.4.1", + "zeroize", +] + [[package]] name = "bs58" version = "0.4.0" @@ -895,7 +921,7 @@ dependencies = [ name = "credentials" version = "0.1.0" dependencies = [ - "bls12_381", + "bls12_381 0.5.0", "coconut-interface", "cosmrs 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", "crypto", @@ -1131,9 +1157,9 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "3.2.1" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f9d052967f590a76e62eb387bd0bbb1b000182c3cefe5364db6b7211651bc0" +checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" dependencies = [ "byteorder", "digest 0.9.0", @@ -1317,6 +1343,27 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" +[[package]] +name = "dkg" +version = "0.1.0" +dependencies = [ + "bitvec 1.0.0", + "bls12_381 0.6.0", + "bs58", + "criterion", + "ff 0.11.0", + "group 0.11.0", + "lazy_static", + "rand 0.8.5", + "rand_chacha 0.3.1", + "rand_core 0.6.3", + "serde", + "serde_derive", + "sha2", + "thiserror", + "zeroize", +] + [[package]] name = "doc-comment" version = "0.3.3" @@ -1753,6 +1800,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.3.21" @@ -2042,6 +2095,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5ac374b108929de78460075f3dc439fa66df9d8fc77e8f12caa5165fcf0c89" dependencies = [ + "byteorder", "ff 0.11.0", "rand_core 0.6.3", "subtle 2.4.1", @@ -3199,7 +3253,7 @@ name = "nymcoconut" version = "0.5.0" dependencies = [ "bincode", - "bls12_381", + "bls12_381 0.5.0", "bs58", "criterion", "digest 0.9.0", @@ -3427,6 +3481,15 @@ dependencies = [ "group 0.10.0", ] +[[package]] +name = "pairing" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2e415e349a3006dd7d9482cdab1c980a845bed1377777d768cb693a44540b42" +dependencies = [ + "group 0.11.0", +] + [[package]] name = "parity-scale-codec" version = "2.3.1" @@ -3434,7 +3497,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "373b1a4c1338d9cd3d1fa53b3a11bdab5ab6bd80a20f7f7becd76953ae2be909" dependencies = [ "arrayvec 0.7.2", - "bitvec", + "bitvec 0.20.4", "byte-slice-cast", "impl-trait-for-tuples", "parity-scale-codec-derive", @@ -3953,6 +4016,12 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "643f8f41a8ebc4c5dc4515c82bb8abd397b527fc20fd681b7c011c2aee5d44fb" +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.6.5" @@ -6508,10 +6577,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" [[package]] -name = "x25519-dalek" -version = "1.2.0" +name = "wyz" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2392b6b94a576b4e2bf3c5b2757d63f10ada8020a2e4d08ac849ebcf6ea8e077" +checksum = "30b31594f29d27036c383b53b59ed3476874d518f0efb151b27a4c275141390e" +dependencies = [ + "tap", +] + +[[package]] +name = "x25519-dalek" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a0c105152107e3b96f6a00a65e86ce82d9b125230e1c4302940eca58ff71f4f" dependencies = [ "curve25519-dalek", "rand_core 0.5.1", @@ -6535,9 +6613,9 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" [[package]] name = "zeroize" -version = "1.3.0" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd" +checksum = "d68d9dcec5f9b43a30d38c49f91dfedfaac384cb8f085faca366c26207dd1619" dependencies = [ "zeroize_derive", ] diff --git a/Cargo.toml b/Cargo.toml index aae00201af..43e8273e46 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ members = [ "common/config", "common/credentials", "common/crypto", + "common/crypto/dkg", "common/bandwidth-claim-contract", "common/cosmwasm-smart-contracts/coconut-bandwidth-contract", "common/cosmwasm-smart-contracts/contracts-common", diff --git a/common/crypto/dkg/Cargo.toml b/common/crypto/dkg/Cargo.toml new file mode 100644 index 0000000000..610e41be19 --- /dev/null +++ b/common/crypto/dkg/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "dkg" +version = "0.1.0" +edition = "2021" +resolver = "2" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bitvec = "1.0.0" + +# unfortunately until https://github.com/zkcrypto/bls12_381/issues/10 is resolved, we have to rely on the fork +# as we need to be able to serialize Gt so that we could create the lookup table for baby-step-giant-step algorithm +bls12_381 = { git = "https://github.com/jstuczyn/bls12_381", branch ="gt-serialisation", default-features = false, features = ["alloc", "pairings", "experimental", "zeroize"] } + +bs58 = "0.4" + +lazy_static = "1.4.0" +rand = { version = "0.8.5", default-features = false} +rand_chacha = "0.3" +rand_core = "0.6.3" +sha2 = "0.9" +serde = "1.0" +serde_derive = "1.0" +thiserror = "1.0" +zeroize = { version = "1.4", features = ["zeroize_derive"] } + +[dependencies.group] +version = "0.11" +default-features = false + +[dependencies.ff] +version = "0.11" +default-features = false + +[dev-dependencies] +criterion = "0.3" + +[[bench]] +name = "benchmarks" +harness = false \ No newline at end of file diff --git a/common/crypto/dkg/benches/benchmarks.rs b/common/crypto/dkg/benches/benchmarks.rs new file mode 100644 index 0000000000..fac5d022a0 --- /dev/null +++ b/common/crypto/dkg/benches/benchmarks.rs @@ -0,0 +1,541 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use bls12_381::{G1Projective, G2Affine, G2Prepared, Scalar}; +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use dkg::bte::encryption::BabyStepGiantStepLookup; +use dkg::bte::proof_chunking::ProofOfChunking; +use dkg::bte::proof_discrete_log::ProofOfDiscreteLog; +use dkg::bte::proof_sharing::ProofOfSecretSharing; +use dkg::bte::{ + decrypt_share, encrypt_shares, keygen, proof_chunking, proof_sharing, setup, DecryptionKey, + Epoch, PublicKey, +}; +use dkg::interpolation::polynomial::Polynomial; +use dkg::{Dealing, NodeIndex, Share}; +use ff::Field; +use rand_core::{RngCore, SeedableRng}; +use std::collections::BTreeMap; + +pub fn precompute_default_bsgs_table(c: &mut Criterion) { + c.bench_function("bsgs default table", |b| { + b.iter(|| black_box(BabyStepGiantStepLookup::default())) + }); +} + +pub fn precomputing_g2_generator_for_miller_loop(c: &mut Criterion) { + let g2 = G2Affine::generator(); + c.bench_function("precomputing G2Prepared", |b| { + b.iter(|| black_box(G2Prepared::from(g2))) + }); +} + +fn prepare_keys( + mut rng: impl RngCore, + nodes: usize, +) -> (BTreeMap, Vec) { + let params = setup(); + let mut node_indices = (0..nodes).map(|_| rng.next_u64()).collect::>(); + node_indices.sort_unstable(); + + let mut receivers = BTreeMap::new(); + let mut dks = Vec::new(); + for index in &node_indices { + let (dk, pk) = keygen(¶ms, &mut rng); + receivers.insert(*index, *pk.public_key()); + dks.push(dk) + } + + (receivers, dks) +} + +pub fn creating_dealing_for_3_parties(c: &mut Criterion) { + let dummy_seed = [42u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + let params = setup(); + let threshold = 2; + let epoch = Epoch::new(2); + + let (receivers, _) = prepare_keys(&mut rng, 3); + + c.bench_function("creating single dealing for 3 parties (threshold 2)", |b| { + b.iter(|| { + black_box({ + Dealing::create( + &mut rng, + ¶ms, + receivers.keys().next().copied().unwrap(), + threshold, + epoch, + &receivers, + ) + }) + }) + }); +} + +pub fn verifying_dealing_made_for_3_parties_and_recovering_share(c: &mut Criterion) { + let dummy_seed = [42u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + let params = setup(); + let threshold = 2; + let epoch = Epoch::new(2); + + let (receivers, mut dks) = prepare_keys(&mut rng, 3); + let (dealing, _) = Dealing::create( + &mut rng, + ¶ms, + receivers.keys().next().copied().unwrap(), + threshold, + epoch, + &receivers, + ); + + let first_key = dks.get_mut(0).unwrap(); + first_key.try_update_to(epoch, ¶ms, &mut rng).unwrap(); + + c.bench_function( + "verifying single dealing made for 3 parties (threshold 2) and recovering share", + |b| { + b.iter(|| { + assert!(dealing + .verify(¶ms, epoch, threshold, &receivers) + .is_ok()); + black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, epoch, None).unwrap()); + }) + }, + ); +} + +pub fn creating_dealing_for_20_parties(c: &mut Criterion) { + let dummy_seed = [42u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + let params = setup(); + let threshold = 14; + let epoch = Epoch::new(2); + + let (receivers, _) = prepare_keys(&mut rng, 20); + + c.bench_function( + "creating single dealing for 20 parties (threshold 14)", + |b| { + b.iter(|| { + black_box({ + Dealing::create( + &mut rng, + ¶ms, + receivers.keys().next().copied().unwrap(), + threshold, + epoch, + &receivers, + ) + }) + }) + }, + ); +} + +pub fn verifying_dealing_made_for_20_parties_and_recovering_share(c: &mut Criterion) { + let dummy_seed = [42u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + let params = setup(); + let threshold = 14; + let epoch = Epoch::new(2); + + let (receivers, mut dks) = prepare_keys(&mut rng, 20); + let (dealing, _) = Dealing::create( + &mut rng, + ¶ms, + receivers.keys().next().copied().unwrap(), + threshold, + epoch, + &receivers, + ); + + let first_key = dks.get_mut(0).unwrap(); + first_key.try_update_to(epoch, ¶ms, &mut rng).unwrap(); + + c.bench_function( + "verifying single dealing made for 20 parties (threshold 14) and recovering share", + |b| { + b.iter(|| { + assert!(dealing + .verify(¶ms, epoch, threshold, &receivers) + .is_ok()); + black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, epoch, None).unwrap()); + }) + }, + ); +} + +pub fn creating_dealing_for_100_parties(c: &mut Criterion) { + let dummy_seed = [42u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + let params = setup(); + let threshold = 67; + let epoch = Epoch::new(2); + + let (receivers, _) = prepare_keys(&mut rng, 100); + + c.bench_function( + "creating single dealing for 100 parties (threshold 67)", + |b| { + b.iter(|| { + black_box({ + Dealing::create( + &mut rng, + ¶ms, + receivers.keys().next().copied().unwrap(), + threshold, + epoch, + &receivers, + ) + }) + }) + }, + ); +} + +pub fn verifying_dealing_made_for_100_parties_and_recovering_share(c: &mut Criterion) { + let dummy_seed = [42u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + let params = setup(); + let threshold = 67; + let epoch = Epoch::new(2); + + let (receivers, mut dks) = prepare_keys(&mut rng, 100); + let (dealing, _) = Dealing::create( + &mut rng, + ¶ms, + receivers.keys().next().copied().unwrap(), + threshold, + epoch, + &receivers, + ); + + let first_key = dks.get_mut(0).unwrap(); + first_key.try_update_to(epoch, ¶ms, &mut rng).unwrap(); + + c.bench_function( + "verifying single dealing made for 100 parties (threshold 67) and recovering share", + |b| { + b.iter(|| { + assert!(dealing + .verify(¶ms, epoch, threshold, &receivers) + .is_ok()); + black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, epoch, None).unwrap()); + }) + }, + ); +} + +pub fn creating_proof_of_key_possession(c: &mut Criterion) { + let dummy_seed = [42u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + + let g1 = G1Projective::generator(); + let x = Scalar::random(&mut rng); + let y = g1 * x; + + c.bench_function("creating proof of key possession", |b| { + b.iter(|| black_box(ProofOfDiscreteLog::construct(&mut rng, &y, &x))) + }); +} + +pub fn verifying_proof_of_key_possession(c: &mut Criterion) { + let dummy_seed = [42u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + + let g1 = G1Projective::generator(); + let x = Scalar::random(&mut rng); + let y = g1 * x; + + let zk_proof = ProofOfDiscreteLog::construct(&mut rng, &y, &x); + c.bench_function("verifying proof of key possession", |b| { + b.iter(|| black_box(zk_proof.verify(&y))) + }); +} + +pub fn creating_proof_of_chunking_for_100_parties(c: &mut Criterion) { + let dummy_seed = [42u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + let params = setup(); + let epoch = Epoch::new(2); + + let (receivers, _) = prepare_keys(&mut rng, 100); + + let polynomial = Polynomial::new_random(&mut rng, 67); + let shares = receivers + .keys() + .map(|&node_index| polynomial.evaluate_at(&Scalar::from(node_index)).into()) + .collect::>(); + + let remote_share_key_pairs = shares + .iter() + .zip(receivers.values()) + .map(|(share, key)| (share, key)) + .collect::>(); + let ordered_public_keys = receivers.values().copied().collect::>(); + + let (ciphertexts, hazmat) = encrypt_shares(&remote_share_key_pairs, epoch, ¶ms, &mut rng); + + c.bench_function("creating proof of chunking for 100 parties", |b| { + b.iter(|| { + let chunking_instance = + proof_chunking::Instance::new(&ordered_public_keys, &ciphertexts); + black_box( + ProofOfChunking::construct(&mut rng, chunking_instance, hazmat.r(), &shares) + .expect("failed to construct proof of chunking"), + ) + }) + }); +} + +pub fn verifying_proof_of_chunking_for_100_parties(c: &mut Criterion) { + let dummy_seed = [42u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + let params = setup(); + let epoch = Epoch::new(2); + + let (receivers, _) = prepare_keys(&mut rng, 100); + + let polynomial = Polynomial::new_random(&mut rng, 67); + let shares = receivers + .keys() + .map(|&node_index| polynomial.evaluate_at(&Scalar::from(node_index)).into()) + .collect::>(); + + let remote_share_key_pairs = shares + .iter() + .zip(receivers.values()) + .map(|(share, key)| (share, key)) + .collect::>(); + let ordered_public_keys = receivers.values().copied().collect::>(); + + let (ciphertexts, hazmat) = encrypt_shares(&remote_share_key_pairs, epoch, ¶ms, &mut rng); + + let chunking_instance = proof_chunking::Instance::new(&ordered_public_keys, &ciphertexts); + let proof_of_chunking = + ProofOfChunking::construct(&mut rng, chunking_instance, hazmat.r(), &shares) + .expect("failed to construct proof of chunking"); + + c.bench_function("verifying proof of chunking for 100 parties", |b| { + b.iter(|| { + let chunking_instance = + proof_chunking::Instance::new(&ordered_public_keys, &ciphertexts); + black_box(proof_of_chunking.verify(chunking_instance)) + }) + }); +} + +pub fn creating_proof_of_secret_sharing_for_100_parties(c: &mut Criterion) { + let dummy_seed = [42u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + let params = setup(); + let epoch = Epoch::new(2); + + let (receivers, _) = prepare_keys(&mut rng, 100); + + let polynomial = Polynomial::new_random(&mut rng, 67); + let shares = receivers + .keys() + .map(|&node_index| polynomial.evaluate_at(&Scalar::from(node_index)).into()) + .collect::>(); + + let remote_share_key_pairs = shares + .iter() + .zip(receivers.values()) + .map(|(share, key)| (share, key)) + .collect::>(); + + let (ciphertexts, hazmat) = encrypt_shares(&remote_share_key_pairs, epoch, ¶ms, &mut rng); + + let combined_ciphertexts = ciphertexts.combine_ciphertexts(); + let combined_r = hazmat.combine_rs(); + let combined_rr = ciphertexts.combine_rs(); + let public_coefficients = polynomial.public_coefficients(); + + c.bench_function("creating proof of secret sharing for 100 parties", |b| { + b.iter(|| { + let sharing_instance = proof_sharing::Instance::new( + &receivers, + &public_coefficients, + &combined_rr, + &combined_ciphertexts, + ); + black_box( + ProofOfSecretSharing::construct(&mut rng, sharing_instance, &combined_r, &shares) + .expect("failed to construct proof of secret sharing"), + ) + }) + }); +} + +pub fn verifying_proof_of_secret_sharing_for_100_parties(c: &mut Criterion) { + let dummy_seed = [42u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + let params = setup(); + let epoch = Epoch::new(2); + + let (receivers, _) = prepare_keys(&mut rng, 100); + + let polynomial = Polynomial::new_random(&mut rng, 67); + let shares = receivers + .keys() + .map(|&node_index| polynomial.evaluate_at(&Scalar::from(node_index)).into()) + .collect::>(); + + let remote_share_key_pairs = shares + .iter() + .zip(receivers.values()) + .map(|(share, key)| (share, key)) + .collect::>(); + + let (ciphertexts, hazmat) = encrypt_shares(&remote_share_key_pairs, epoch, ¶ms, &mut rng); + + let combined_ciphertexts = ciphertexts.combine_ciphertexts(); + let combined_r = hazmat.combine_rs(); + let combined_rr = ciphertexts.combine_rs(); + let public_coefficients = polynomial.public_coefficients(); + let sharing_instance = proof_sharing::Instance::new( + &receivers, + &public_coefficients, + &combined_rr, + &combined_ciphertexts, + ); + let proof_of_secret_sharing = + ProofOfSecretSharing::construct(&mut rng, sharing_instance, &combined_r, &shares) + .expect("failed to construct proof of secret sharing"); + + c.bench_function("verifying proof of secret sharing for 100 parties", |b| { + b.iter(|| { + let sharing_instance = proof_sharing::Instance::new( + &receivers, + &public_coefficients, + &combined_rr, + &combined_ciphertexts, + ); + black_box(proof_of_secret_sharing.verify(sharing_instance)) + }) + }); +} + +pub fn single_share_encryption(c: &mut Criterion) { + let dummy_seed = [42u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + let params = setup(); + let epoch = Epoch::new(2); + let (_, pk) = keygen(¶ms, &mut rng); + + let polynomial = Polynomial::new_random(&mut rng, 3); + let share: Share = polynomial.evaluate_at(&Scalar::from(42)).into(); + + c.bench_function("single share encryption", |b| { + b.iter(|| { + black_box(encrypt_shares( + &[(&share, pk.public_key())], + epoch, + ¶ms, + &mut rng, + )) + }) + }); +} + +pub fn share_encryption_100(c: &mut Criterion) { + let dummy_seed = [42u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + let params = setup(); + let epoch = Epoch::new(2); + + let (receivers, _) = prepare_keys(&mut rng, 100); + let polynomial = Polynomial::new_random(&mut rng, 3); + let shares = receivers + .keys() + .map(|&node_index| polynomial.evaluate_at(&Scalar::from(node_index)).into()) + .collect::>(); + + let remote_share_key_pairs = shares + .iter() + .zip(receivers.values()) + .map(|(share, key)| (share, key)) + .collect::>(); + + c.bench_function("100 shares encryption", |b| { + b.iter(|| { + black_box(encrypt_shares( + &remote_share_key_pairs, + epoch, + ¶ms, + &mut rng, + )) + }) + }); +} + +pub fn share_decryption(c: &mut Criterion) { + let dummy_seed = [42u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + let params = setup(); + let epoch = Epoch::new(2); + let (mut dk, pk) = keygen(¶ms, &mut rng); + + let polynomial = Polynomial::new_random(&mut rng, 3); + let share: Share = polynomial.evaluate_at(&Scalar::from(42)).into(); + let (ciphertexts, _) = encrypt_shares(&[(&share, pk.public_key())], epoch, ¶ms, &mut rng); + dk.try_update_to(epoch, ¶ms, &mut rng).unwrap(); + + c.bench_function("single share decryption", |b| { + b.iter(|| black_box(decrypt_share(&dk, 0, &ciphertexts, epoch, None))) + }); +} + +criterion_group!( + utils, + precompute_default_bsgs_table, + precomputing_g2_generator_for_miller_loop, +); + +criterion_group!( + dealings_creation, + creating_dealing_for_3_parties, + creating_dealing_for_20_parties, + creating_dealing_for_100_parties, +); + +// note: in our setting each party will have to create at least 4 dealings (one per attribute in credential) +// and verify 99 * 4 of them (4 from each other dealer) +criterion_group!( + dealings_verification, + verifying_dealing_made_for_3_parties_and_recovering_share, + verifying_dealing_made_for_20_parties_and_recovering_share, + verifying_dealing_made_for_100_parties_and_recovering_share, +); + +criterion_group!( + proofs_of_knowledge, + creating_proof_of_key_possession, + verifying_proof_of_key_possession, + creating_proof_of_chunking_for_100_parties, + verifying_proof_of_chunking_for_100_parties, + creating_proof_of_secret_sharing_for_100_parties, + verifying_proof_of_secret_sharing_for_100_parties +); + +criterion_group!( + encryption, + single_share_encryption, + share_encryption_100, + share_decryption, +); + +criterion_main!( + utils, + dealings_creation, + dealings_verification, + proofs_of_knowledge, + encryption +); + +// TODO: benchmark using affine vs projective representation throughout the crate +// (when conversion / serialization / computation is involved) diff --git a/common/crypto/dkg/src/bte/encryption.rs b/common/crypto/dkg/src/bte/encryption.rs new file mode 100644 index 0000000000..cb8a8304cd --- /dev/null +++ b/common/crypto/dkg/src/bte/encryption.rs @@ -0,0 +1,772 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::bte::keys::{DecryptionKey, PublicKey}; +use crate::bte::{Epoch, Params, CHUNK_SIZE, G2_GENERATOR_PREPARED, NUM_CHUNKS, PAIRING_BASE}; +use crate::error::DkgError; +use crate::utils::{combine_g1_chunks, combine_scalar_chunks, deserialize_g1, deserialize_g2}; +use crate::{Chunk, ChunkedShare, Share}; +use bls12_381::{G1Affine, G1Projective, G2Prepared, G2Projective, Gt, Scalar}; +use ff::Field; +use group::{Curve, Group, GroupEncoding}; +use rand_core::RngCore; +use std::collections::HashMap; +use std::ops::Neg; +use zeroize::Zeroize; + +#[derive(Debug)] +#[cfg_attr(test, derive(Clone, PartialEq))] +pub struct Ciphertexts { + pub rr: [G1Projective; NUM_CHUNKS], + pub ss: [G1Projective; NUM_CHUNKS], + pub zz: [G2Projective; NUM_CHUNKS], + pub ciphertext_chunks: Vec<[G1Projective; NUM_CHUNKS]>, +} + +impl Ciphertexts { + pub fn verify_integrity(&self, params: &Params, epoch: Epoch) -> bool { + // if this checks fails it means the ciphertext is undefined as values + // in `r`, `s` and `z` are meaningless since technically this ciphertext + // has been created for 0 parties + if self.ciphertext_chunks.is_empty() { + return false; + } + + let g1_neg = G1Affine::generator().neg(); + let f = epoch + .as_extended_tau(&self.rr, &self.ss, &self.ciphertext_chunks) + .evaluate_f(params); + + // we have to use `f` in up to `NUM_CHUNKS` pairings (if everything is valid), + // so perform some precomputation on it + let f_prepared = G2Prepared::from(f.to_affine()); + + // for each triple (R_i, S_i, Z_i) check whether e(g1, Z_i) == e(R_j, f) • e(S_i, h), + // which is equivalent to checking whether e(R_j, f) • e(S_i, h) • e(g1, Z_i)^-1 == id + // and due to bilinear property whether e(R_j, f) • e(S_i, h) • e(g1^-1, Z_i) == id + for i in 0..self.rr.len() { + let miller = bls12_381::multi_miller_loop(&[ + (&self.rr[i].to_affine(), &f_prepared), + (&self.ss[i].to_affine(), ¶ms._h_prepared), + (&g1_neg, &G2Prepared::from(self.zz[i].to_affine())), + ]); + let res = miller.final_exponentiation(); + if !bool::from(res.is_identity()) { + return false; + } + } + + true + } + + pub fn combine_rs(&self) -> G1Projective { + combine_g1_chunks(&self.rr) + } + + // required for the purposes of the proof of secret sharing + pub fn combine_ciphertexts(&self) -> Vec { + self.ciphertext_chunks + .iter() + .map(|share_ciphertext| combine_g1_chunks(share_ciphertext)) + .collect() + } + + pub(crate) fn to_bytes(&self) -> Vec { + let num_receivers = self.ciphertext_chunks.len(); + + let mut bytes = Vec::with_capacity(NUM_CHUNKS * ((num_receivers + 2) * 48 + 96) + 4); + for r_i in &self.rr { + bytes.extend_from_slice(r_i.to_bytes().as_ref()) + } + for s_i in &self.ss { + bytes.extend_from_slice(s_i.to_bytes().as_ref()) + } + for z_i in &self.zz { + bytes.extend_from_slice(z_i.to_bytes().as_ref()) + } + + bytes.extend_from_slice(&(num_receivers as u32).to_be_bytes()); + for c_i in &self.ciphertext_chunks { + for c_ij in c_i { + bytes.extend_from_slice(c_ij.to_bytes().as_ref()) + } + } + + bytes + } + + pub(crate) fn try_from_bytes(bytes: &[u8]) -> Result { + // at the very minimum we must have enough bytes for a single receiver + if bytes.len() < NUM_CHUNKS * (3 * 48 + 96) + 4 { + return Err(DkgError::new_deserialization_failure( + "Ciphertexts", + "insufficient number of bytes provided", + )); + } + + let mut rr = Vec::with_capacity(NUM_CHUNKS); + let mut ss = Vec::with_capacity(NUM_CHUNKS); + let mut zz = Vec::with_capacity(NUM_CHUNKS); + + let mut i = 0; + for _ in 0..NUM_CHUNKS { + rr.push(deserialize_g1(&bytes[i..i + 48]).ok_or_else(|| { + DkgError::new_deserialization_failure("Ciphertexts.r", "invalid curve point") + })?); + i += 48; + } + for _ in 0..NUM_CHUNKS { + ss.push(deserialize_g1(&bytes[i..i + 48]).ok_or_else(|| { + DkgError::new_deserialization_failure("Ciphertexts.s", "invalid curve point") + })?); + i += 48; + } + for _ in 0..NUM_CHUNKS { + zz.push(deserialize_g2(&bytes[i..i + 96]).ok_or_else(|| { + DkgError::new_deserialization_failure("Ciphertexts.z", "invalid curve point") + })?); + i += 96; + } + + let num_receivers = u32::from_be_bytes(bytes[i..i + 4].try_into().unwrap()) as usize; + i += 4; + + if bytes[i..].len() != num_receivers * NUM_CHUNKS * 48 { + return Err(DkgError::new_deserialization_failure( + "Ciphertexts", + "invalid number of bytes provided", + )); + } + + let mut ciphertext_chunks = Vec::with_capacity(num_receivers); + + for _ in 0..num_receivers { + let mut ci = Vec::with_capacity(NUM_CHUNKS); + for _ in 0..NUM_CHUNKS { + ci.push(deserialize_g1(&bytes[i..i + 48]).ok_or_else(|| { + DkgError::new_deserialization_failure( + "Ciphertexts.ciphertext_chunks", + "invalid curve point", + ) + })?); + i += 48; + } + + // this unwrap is fine as we have exactly NUM_CHUNKS elements in each vector + ciphertext_chunks.push(ci.try_into().unwrap()) + } + + // and the same is true here, the unwraps are fine as we have exactly NUM_CHUNKS elements in each as required + Ok(Ciphertexts { + rr: rr.try_into().unwrap(), + ss: ss.try_into().unwrap(), + zz: zz.try_into().unwrap(), + ciphertext_chunks, + }) + } +} + +#[derive(Zeroize)] +#[zeroize(drop)] +/// Randomness generated during ciphertext generation that is required for proofs of knowledge. +/// It must be handled with extreme care as its misuse might help malicious parties to recover +/// the underlying plaintext. +pub struct HazmatRandomness { + r: [Scalar; NUM_CHUNKS], + s: [Scalar; NUM_CHUNKS], +} + +impl HazmatRandomness { + pub fn r(&self) -> &[Scalar; NUM_CHUNKS] { + &self.r + } + + pub fn s(&self) -> &[Scalar; NUM_CHUNKS] { + &self.s + } + + pub fn combine_rs(&self) -> Scalar { + combine_scalar_chunks(&self.r) + } +} + +pub fn encrypt_shares( + shares: &[(&Share, &PublicKey)], + epoch: Epoch, + params: &Params, + mut rng: impl RngCore, +) -> (Ciphertexts, HazmatRandomness) { + let g1 = G1Projective::generator(); + + let mut rand_rs = Vec::with_capacity(NUM_CHUNKS); + let mut rand_ss = Vec::with_capacity(NUM_CHUNKS); + let mut rr = Vec::with_capacity(NUM_CHUNKS); + let mut ss = Vec::with_capacity(NUM_CHUNKS); + + // generate relevant re-usable pseudorandom data + for _ in 0..NUM_CHUNKS { + let rand_r = Scalar::random(&mut rng); + let rand_s = Scalar::random(&mut rng); + + // g1^r + let rr_i = g1 * rand_r; + // g1^s + let ss_i = g1 * rand_s; + + rand_rs.push(rand_r); + rand_ss.push(rand_s); + + rr.push(rr_i); + ss.push(ss_i); + } + + // produce per-chunk ciphertexts + let mut cc = Vec::with_capacity(shares.len()); + + for (share, pk) in shares { + let m = share.to_chunks(); + + let mut ci = Vec::with_capacity(NUM_CHUNKS); + + for (j, chunk) in m.chunks.iter().enumerate() { + // can't really have a more efficient implementation until https://github.com/zkcrypto/bls12_381/pull/70 is merged... + let c = pk.0 * rand_rs[j] + g1 * Scalar::from(*chunk as u64); + ci.push(c) + } + + // the conversion must succeed since we must have EXACTLY `NUM_CHUNKS` elements + cc.push(ci.try_into().unwrap()) + } + + // convert into arrays, note that the unwraps are fine as we have exactly `NUM_CHUNKS` elements in each vector + let rr = rr.try_into().unwrap(); + let ss = ss.try_into().unwrap(); + + let f = epoch.as_extended_tau(&rr, &ss, &cc).evaluate_f(params); + + let mut zz = Vec::with_capacity(NUM_CHUNKS); + for i in 0..NUM_CHUNKS { + zz.push(f * rand_rs[i] + params.h * rand_ss[i]); + } + + // the conversions here must also succeed since the other vecs also have `NUM_CHUNKS` elements + ( + Ciphertexts { + rr, + ss, + zz: zz.try_into().unwrap(), + ciphertext_chunks: cc, + }, + HazmatRandomness { + r: rand_rs.try_into().unwrap(), + s: rand_ss.try_into().unwrap(), + }, + ) +} + +pub fn decrypt_share( + dk: &DecryptionKey, + // in the case of multiple receivers, specifies which index of ciphertext chunks should be used + i: usize, + ciphertext: &Ciphertexts, + epoch: Epoch, + lookup_table: Option<&BabyStepGiantStepLookup>, +) -> Result { + let mut plaintext = ChunkedShare::default(); + + let decryption_node = dk.try_get_compatible_node(epoch)?; + let extended_tau = epoch.as_extended_tau( + &ciphertext.rr, + &ciphertext.ss, + &ciphertext.ciphertext_chunks, + ); + + if i >= ciphertext.ciphertext_chunks.len() { + return Err(DkgError::UnavailableCiphertext(i)); + } + + let height = decryption_node.tau.height(); + let b_neg = decryption_node + .ds + .iter() + .chain(decryption_node.dh.iter()) + .zip(extended_tau.0.iter().by_vals().skip(height)) + .filter(|(_, i)| *i) + .map(|(d_i, _)| d_i) + .fold(decryption_node.b, |acc, d_i| acc + d_i) + .neg() + .to_affine(); + + let e_neg = decryption_node.e.neg().to_affine(); + + for j in 0..NUM_CHUNKS { + let rr_j = &ciphertext.rr[j]; + let ss_j = &ciphertext.ss[j]; + let zz_j = ciphertext.zz[j].to_affine(); + let cc_ij = &ciphertext.ciphertext_chunks[i][j]; + + let miller = bls12_381::multi_miller_loop(&[ + (&cc_ij.to_affine(), &G2_GENERATOR_PREPARED), + (&rr_j.to_affine(), &G2Prepared::from(b_neg)), + (&decryption_node.a.to_affine(), &G2Prepared::from(zz_j)), + (&ss_j.to_affine(), &G2Prepared::from(e_neg)), + ]); + let m = miller.final_exponentiation(); + + plaintext.chunks[j] = baby_step_giant_step(&m, &PAIRING_BASE, lookup_table)?; + } + + plaintext.try_into() +} + +pub struct BabyStepGiantStepLookup { + base: Gt, + m: Chunk, + lookup: HashMap<[u8; 576], Chunk>, +} + +impl BabyStepGiantStepLookup { + pub fn precompute(base: &Gt) -> Self { + let mut lookup = HashMap::new(); + let mut g = Gt::identity(); + + // 1. m ← Ceiling(√n) + let m = (CHUNK_SIZE as f32).sqrt().ceil() as Chunk; + + // 2. For all j where 0 ≤ j < m: + for j in 0..m { + // Compute α^j and store the pair (j, α^j) in a table. + lookup.insert(g.to_uncompressed(), j); + g += base; + } + + BabyStepGiantStepLookup { + base: *base, + m, + lookup, + } + } + + pub fn try_solve(&self, target: &Gt) -> Result { + // 3. Compute α^{−m} + let m_neg = Scalar::from(self.m as u64).neg(); + let alpha_m = self.base * m_neg; + + // 4. γ ← β. (set γ = β) + let mut gamma = *target; + + // 5. For all i where 0 ≤ i < m: + for i in 0..self.m { + // 1. Check to see if γ is the second component (αj) of any pair in the table. + if let Some(j) = self.lookup.get(&gamma.to_uncompressed()) { + // 2. If so, return im + j. + return Ok(i * self.m + j); + } else { + // 3. If not, γ ← γ • α^{−m}. + gamma += alpha_m; + } + } + + Err(DkgError::UnsolvableDiscreteLog) + } +} + +impl Default for BabyStepGiantStepLookup { + fn default() -> Self { + BabyStepGiantStepLookup::precompute(&PAIRING_BASE) + } +} + +/// Attempts to solve the discrete log problem g^m, where g is in the Gt group and +/// m should be within the [0, CHUNK_MAX] range. +/// +/// The implementation follows the following algorithm: https://en.wikipedia.org/wiki/Baby-step_giant-step#The_algorithm +/// +/// # Arguments +/// +/// * `target`: the result of the exponentiation, M in M = g^m, +/// * `base`: the base used for exponentiation, g in M = g^m +/// * `lookup_table`: precomputed table containing (j, α^j) pairs +pub fn baby_step_giant_step( + target: &Gt, + base: &Gt, + lookup_table: Option<&BabyStepGiantStepLookup>, +) -> Result { + if let Some(lookup_table) = lookup_table { + // compute expected m to make sure the provided lookup is valid + let m = (CHUNK_SIZE as f32).sqrt().ceil() as Chunk; + + if &lookup_table.base != base || lookup_table.lookup.len() != m as usize { + return Err(DkgError::MismatchedLookupTable); + } + + lookup_table.try_solve(target) + } else { + BabyStepGiantStepLookup::precompute(base).try_solve(target) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::bte::{keygen, setup, DEFAULT_BSGS_TABLE}; + use rand_core::SeedableRng; + + fn verify_hazmat_rand(ciphertext: &Ciphertexts, randomness: &HazmatRandomness) { + let g1 = G1Projective::generator(); + + for i in 0..ciphertext.rr.len() { + assert_eq!(ciphertext.rr[i], g1 * randomness.r[i]); + assert_eq!(ciphertext.ss[i], g1 * randomness.s[i]); + } + } + + #[test] + fn baby_giant_100_without_table() { + let dummy_seed = [1u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + + for i in 0u64..100 { + let base = Gt::random(&mut rng); + let x = (rng.next_u64() + i) % CHUNK_SIZE as u64; + let target = base * Scalar::from(x); + + assert_eq!( + baby_step_giant_step(&target, &base, None).unwrap(), + x as Chunk + ); + } + } + + #[test] + fn baby_giant_100_with_table() { + let dummy_seed = [1u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + let base = Gt::random(&mut rng); + let lookup_table = BabyStepGiantStepLookup::precompute(&base); + let table = Some(&lookup_table); + + for i in 0u64..100 { + let x = (rng.next_u64() + i) % CHUNK_SIZE as u64; + let target = base * Scalar::from(x); + + assert_eq!( + baby_step_giant_step(&target, &base, table).unwrap(), + x as Chunk + ); + } + } + + #[test] + fn share_decryption_20() { + let dummy_seed = [1u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + let params = setup(); + + let (decryption_key1, public_key1) = keygen(¶ms, &mut rng); + let (decryption_key2, public_key2) = keygen(¶ms, &mut rng); + let epoch = Epoch::new(0); + + let lookup_table = &DEFAULT_BSGS_TABLE; + + for _ in 0..10 { + let m1 = Share::random(&mut rng); + let m2 = Share::random(&mut rng); + let shares = &[(&m1, &public_key1.key), (&m2, &public_key2.key)]; + + let (ciphertext, hazmat) = encrypt_shares(shares, epoch, ¶ms, &mut rng); + verify_hazmat_rand(&ciphertext, &hazmat); + + let recovered1 = + decrypt_share(&decryption_key1, 0, &ciphertext, epoch, Some(lookup_table)).unwrap(); + let recovered2 = + decrypt_share(&decryption_key2, 1, &ciphertext, epoch, Some(lookup_table)).unwrap(); + assert_eq!(m1, recovered1); + assert_eq!(m2, recovered2); + } + } + + #[test] + fn share_encryption_under_nonzero_epoch() { + let dummy_seed = [1u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + let params = setup(); + + let (mut decryption_key1, public_key1) = keygen(¶ms, &mut rng); + let (mut decryption_key2, public_key2) = keygen(¶ms, &mut rng); + let epoch = Epoch::new(12345); + decryption_key1 + .try_update_to(epoch, ¶ms, &mut rng) + .unwrap(); + decryption_key2 + .try_update_to(epoch, ¶ms, &mut rng) + .unwrap(); + + let lookup_table = &DEFAULT_BSGS_TABLE; + + for _ in 0..10 { + let m1 = Share::random(&mut rng); + let m2 = Share::random(&mut rng); + let shares = &[(&m1, &public_key1.key), (&m2, &public_key2.key)]; + + let (ciphertext, hazmat) = encrypt_shares(shares, epoch, ¶ms, &mut rng); + verify_hazmat_rand(&ciphertext, &hazmat); + + let recovered1 = + decrypt_share(&decryption_key1, 0, &ciphertext, epoch, Some(lookup_table)).unwrap(); + let recovered2 = + decrypt_share(&decryption_key2, 1, &ciphertext, epoch, Some(lookup_table)).unwrap(); + assert_eq!(m1, recovered1); + assert_eq!(m2, recovered2); + } + } + + #[test] + fn decryption_with_root_key() { + let dummy_seed = [42u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + let params = setup(); + + let (root_key, public_key) = keygen(¶ms, &mut rng); + + let share = Share::random(&mut rng); + + let epoch0 = Epoch::new(0); + let epoch42 = Epoch::new(42); + let epoch_big = Epoch::new(3292547435); + + let (ciphertext1, hazmat1) = + encrypt_shares(&[(&share, &public_key.key)], epoch0, ¶ms, &mut rng); + verify_hazmat_rand(&ciphertext1, &hazmat1); + + let (ciphertext2, hazmat2) = + encrypt_shares(&[(&share, &public_key.key)], epoch42, ¶ms, &mut rng); + verify_hazmat_rand(&ciphertext2, &hazmat2); + + let (ciphertext3, hazmat3) = + encrypt_shares(&[(&share, &public_key.key)], epoch_big, ¶ms, &mut rng); + verify_hazmat_rand(&ciphertext3, &hazmat3); + + let recovered1 = decrypt_share(&root_key, 0, &ciphertext1, epoch0, None).unwrap(); + let recovered2 = decrypt_share(&root_key, 0, &ciphertext2, epoch42, None).unwrap(); + let recovered3 = decrypt_share(&root_key, 0, &ciphertext3, epoch_big, None).unwrap(); + + assert_eq!(share, recovered1); + assert_eq!(share, recovered2); + assert_eq!(share, recovered3); + } + + #[test] + fn update_and_decrypt_10() { + let dummy_seed = [1u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + let params = setup(); + + let (mut decryption_key, public_key) = keygen(¶ms, &mut rng); + + for epoch_value in 0..10 { + let epoch = Epoch::new(epoch_value); + let share = Share::random(&mut rng); + decryption_key + .try_update_to(epoch, ¶ms, &mut rng) + .unwrap(); + + let (ciphertext, hazmat) = + encrypt_shares(&[(&share, &public_key.key)], epoch, ¶ms, &mut rng); + verify_hazmat_rand(&ciphertext, &hazmat); + + let recovered = decrypt_share(&decryption_key, 0, &ciphertext, epoch, None).unwrap(); + assert_eq!(share, recovered); + } + } + + #[test] + fn reblinding_node_doesnt_affect_decryption() { + let dummy_seed = [1u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + let params = setup(); + + let (mut decryption_key, public_key) = keygen(¶ms, &mut rng); + + let epoch = Epoch::new(12345); + decryption_key + .try_update_to(epoch, ¶ms, &mut rng) + .unwrap(); + for node in decryption_key.nodes.iter_mut() { + node.reblind(¶ms, &mut rng); + } + let share = Share::random(&mut rng); + + let (ciphertext, hazmat) = + encrypt_shares(&[(&share, &public_key.key)], epoch, ¶ms, &mut rng); + verify_hazmat_rand(&ciphertext, &hazmat); + + let recovered = decrypt_share(&decryption_key, 0, &ciphertext, epoch, None).unwrap(); + assert_eq!(share, recovered); + + // attempt to update the key again so we have to derive fresh nodes using previous reblinded results + let epoch2 = Epoch::new(67890); + decryption_key + .try_update_to(epoch2, ¶ms, &mut rng) + .unwrap(); + for node in decryption_key.nodes.iter_mut() { + node.reblind(¶ms, &mut rng); + } + let share2 = Share::random(&mut rng); + + let (ciphertext, hazmat) = + encrypt_shares(&[(&share2, &public_key.key)], epoch2, ¶ms, &mut rng); + verify_hazmat_rand(&ciphertext, &hazmat); + + let recovered = decrypt_share(&decryption_key, 0, &ciphertext, epoch2, None).unwrap(); + assert_eq!(share2, recovered); + } + + #[test] + fn ciphertext_integrity_check_passes_for_valid_data() { + let params = setup(); + + let dummy_seed = [1u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + + let (mut dk, public_key) = keygen(¶ms, &mut rng); + let epoch = Epoch::new(1); + + dk.try_update_to(epoch, ¶ms, &mut rng).unwrap(); + let share = Share::random(&mut rng); + let (ciphertext, _) = + encrypt_shares(&[(&share, &public_key.key)], epoch, ¶ms, &mut rng); + assert!(ciphertext.verify_integrity(¶ms, epoch)) + } + + #[test] + fn ciphertext_integrity_check_passes_fails_for_malformed_data() { + let params = setup(); + + let dummy_seed = [1u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + + let (mut dk, public_key) = keygen(¶ms, &mut rng); + let epoch = Epoch::new(1); + + dk.try_update_to(epoch, ¶ms, &mut rng).unwrap(); + let share = Share::random(&mut rng); + let (ciphertext, _) = + encrypt_shares(&[(&share, &public_key.key)], epoch, ¶ms, &mut rng); + + let mut bad_cipher1 = ciphertext.clone(); + bad_cipher1.rr[4] = G1Projective::generator(); + assert!(!bad_cipher1.verify_integrity(¶ms, epoch)); + + let mut bad_cipher2 = ciphertext.clone(); + bad_cipher2.ss[4] = G1Projective::generator(); + assert!(!bad_cipher2.verify_integrity(¶ms, epoch)); + + let mut bad_cipher3 = ciphertext; + bad_cipher3.zz[4] = G2Projective::generator(); + assert!(!bad_cipher3.verify_integrity(¶ms, epoch)); + } + + #[test] + fn ciphertext_integrity_check_passes_fails_for_wrong_epoch() { + let params = setup(); + + let dummy_seed = [1u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + + let (mut dk, public_key) = keygen(¶ms, &mut rng); + let epoch = Epoch::new(1); + + dk.try_update_to(epoch, ¶ms, &mut rng).unwrap(); + let share = Share::random(&mut rng); + let (ciphertext, _) = + encrypt_shares(&[(&share, &public_key.key)], epoch, ¶ms, &mut rng); + + let another_epoch = Epoch::new(2); + assert!(!ciphertext.verify_integrity(¶ms, another_epoch)) + } + + #[test] + fn ciphertext_combining() { + let params = setup(); + + let dummy_seed = [1u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + + let nodes = 3; + + let mut shares = Vec::new(); + let mut public_keys = Vec::new(); + for _ in 0..nodes { + shares.push(Share::random(&mut rng)); + let (_, pk) = keygen(¶ms, &mut rng); + public_keys.push(*pk.public_key()); + } + + let refs = shares.iter().zip(public_keys.iter()).collect::>(); + let (ciphertext, hazmat) = encrypt_shares(&refs, Epoch::new(42), ¶ms, &mut rng); + + let combined_r = combine_scalar_chunks(hazmat.r()); + let combined_rr = ciphertext.combine_rs(); + let combined_ciphertexts = ciphertext.combine_ciphertexts(); + + let g1 = G1Projective::generator(); + for i in 0..nodes { + let expected = public_keys[i].0 * combined_r + g1 * shares[i].0; + assert_eq!(expected, combined_ciphertexts[i]); + assert_eq!(combined_rr, g1 * combined_r); + } + } + + #[test] + fn ciphertexts_roundtrip() { + fn random_ciphertexts(mut rng: impl RngCore, num_receivers: usize) -> Ciphertexts { + Ciphertexts { + rr: (0..NUM_CHUNKS) + .map(|_| G1Projective::random(&mut rng)) + .collect::>() + .try_into() + .unwrap(), + ss: (0..NUM_CHUNKS) + .map(|_| G1Projective::random(&mut rng)) + .collect::>() + .try_into() + .unwrap(), + zz: (0..NUM_CHUNKS) + .map(|_| G2Projective::random(&mut rng)) + .collect::>() + .try_into() + .unwrap(), + ciphertext_chunks: (0..num_receivers) + .map(|_| { + (0..NUM_CHUNKS) + .map(|_| G1Projective::random(&mut rng)) + .collect::>() + .try_into() + .unwrap() + }) + .collect(), + } + } + + let dummy_seed = [1u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + + let good_ciphertexts = vec![ + random_ciphertexts(&mut rng, 1), + random_ciphertexts(&mut rng, 2), + random_ciphertexts(&mut rng, 10), + ]; + + for ciphertexts in &good_ciphertexts { + let bytes = ciphertexts.to_bytes(); + let recovered = Ciphertexts::try_from_bytes(&bytes).unwrap(); + assert_eq!(ciphertexts, &recovered); + } + + // ciphertext for 0 receivers is invalid by default + let ciphertexts = random_ciphertexts(&mut rng, 0); + let bytes = ciphertexts.to_bytes(); + assert!(Ciphertexts::try_from_bytes(&bytes).is_err()); + } +} diff --git a/common/crypto/dkg/src/bte/keys.rs b/common/crypto/dkg/src/bte/keys.rs new file mode 100644 index 0000000000..64ef47b010 --- /dev/null +++ b/common/crypto/dkg/src/bte/keys.rs @@ -0,0 +1,875 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::bte::proof_discrete_log::ProofOfDiscreteLog; +use crate::bte::{Epoch, Params, Tau}; +use crate::error::DkgError; +use crate::utils::{deserialize_g1, deserialize_g2, deserialize_scalar}; +use bls12_381::{G1Projective, G2Projective, Scalar}; +use ff::Field; +use group::GroupEncoding; +use rand_core::RngCore; +use zeroize::Zeroize; + +#[derive(Debug, Zeroize)] +#[zeroize(drop)] +#[cfg_attr(test, derive(Clone, PartialEq))] +pub(crate) struct Node { + pub(crate) tau: Tau, + + // g1^rho + pub(crate) a: G1Projective, + + // g2^x + pub(crate) b: G2Projective, + + // f_i^rho, up to lambda_t elements + pub(crate) ds: Vec, + + // fh_i^rho, always lambda_h elements + pub(crate) dh: Vec, + + // h^rho + pub(crate) e: G2Projective, +} + +impl Node { + fn new_root( + a: G1Projective, + b: G2Projective, + ds: Vec, + dh: Vec, + e: G2Projective, + ) -> Self { + Node { + tau: Tau::new_root(), + a, + b, + ds, + dh, + e, + } + } + + fn is_root(&self) -> bool { + self.tau.0.is_empty() + } + + pub(crate) fn reblind(&mut self, params: &Params, mut rng: impl RngCore) { + let delta = Scalar::random(&mut rng); + self.a += G1Projective::generator() * delta; + + // TODO: or do we have to do full tau evaluation here? + self.b += self.tau.evaluate_partial_f(params) * delta; + self.ds + .iter_mut() + .zip(params.fs.iter().skip(self.tau.height())) + .for_each(|(d_i, f_i)| *d_i += f_i * delta); + self.dh + .iter_mut() + .zip(params.fh.iter()) + .for_each(|(d_i, f_i)| *d_i += f_i * delta); + + self.e += params.h * delta; + } + + // note: it's unsafe to use this method outside `try_update_to` as + // we have guaranteed there that `self` is parent of the target + // and that `self.tau != target_tau` + /// Given `self` with `Tau1` and `target_tau` with `Tau2`, such that `Tau1` prefixes `Tau2`, + /// i.e. `Tau2 == Tau1 || SUFFIX`, and `Tau2` is a leaf node, derive all required crypto material + /// for its construction. + fn derive_target_child_with_partials( + &self, + params: &Params, + target_tau: Tau, + partial_b: &G2Projective, + partial_f: &G2Projective, + mut rng: impl RngCore, + ) -> Self { + debug_assert!(self.tau.is_parent_of(&target_tau)); + debug_assert_ne!(self.tau, target_tau); + + let delta = Scalar::random(&mut rng); + let a = self.a + G1Projective::generator() * delta; + let b = partial_b + partial_f * delta; + let ds = self + .ds + .iter() + .zip(params.fs.iter()) + .skip(target_tau.height()) + .map(|(d_i, f_i)| d_i + f_i * delta) + .collect(); + let dh = self + .dh + .iter() + .zip(params.fh.iter()) + .map(|(dh_i, fh_i)| dh_i + fh_i * delta) + .collect(); + let e = self.e + params.h * delta; + + Node { + tau: target_tau, + a, + b, + ds, + dh, + e, + } + } + + // note: it's unsafe to use this method outside `try_update_to` as + // we have guaranteed there that `self` is parent of the target + // and that `self.tau != target_tau` + /// Given `self` with `Tau1` and `most_direct_parent` with `Tau2`, such that `Tau1` prefixes `Tau2`, + /// i.e. `Tau2 == Tau1 || SUFFIX`, derive node with `Tau3 = Tau2 || 1` + fn derive_right_nonfinal_child_of_with_partials( + &self, + params: &Params, + most_direct_parent: Tau, + partial_b: &G2Projective, + partial_f: &G2Projective, + mut rng: impl RngCore, + ) -> Self { + let right_branch = most_direct_parent.right_child(); + + debug_assert!(self.tau.is_parent_of(&most_direct_parent)); + debug_assert!(self.tau.is_parent_of(&right_branch)); + debug_assert_ne!(self.tau, right_branch); + + // n is height difference between self and the child + let n = right_branch.height() - self.tau.height(); + + // i is the index of the last bit we just added + let i = right_branch.height() - 1; + + let delta = Scalar::random(&mut rng); + let a = self.a + G1Projective::generator() * delta; + let d0 = self.ds[n - 1]; + let b = partial_b + d0 + (partial_f + params.fs[i]) * delta; + let ds = self + .ds + .iter() + .skip(n) + .zip(params.fs.iter().skip(right_branch.height())) + .map(|(d_i, f_i)| d_i + f_i * delta) + .collect(); + let dh = self + .dh + .iter() + .zip(params.fh.iter()) + .map(|(dh_i, fh_i)| dh_i + fh_i * delta) + .collect(); + + let e = self.e + params.h * delta; + + Node { + tau: right_branch, + a, + b, + ds, + dh, + e, + } + } + + // tau_bytes_len || tau || a || b || len_ds || ds || len_dh || dh || e + pub(crate) fn to_bytes(&self) -> Vec { + let g1_elements = 1; + let g2_elements = self.ds.len() + self.dh.len() + 2; + + let tau_bytes = self.tau.to_bytes(); + + // the extra 12 comes from the triple u32 we use for encoding lengths of tau, ds and dh + let mut bytes = + Vec::with_capacity(tau_bytes.len() + g1_elements * 48 + g2_elements * 96 + 12); + + bytes.extend_from_slice(&((tau_bytes.len() as u32).to_be_bytes())); + bytes.extend_from_slice(&tau_bytes); + bytes.extend_from_slice(self.a.to_bytes().as_ref()); + bytes.extend_from_slice(self.b.to_bytes().as_ref()); + bytes.extend_from_slice(&((self.ds.len() as u32).to_be_bytes())); + for d_i in &self.ds { + bytes.extend_from_slice(d_i.to_bytes().as_ref()); + } + bytes.extend_from_slice(&((self.dh.len() as u32).to_be_bytes())); + for dh_i in &self.dh { + bytes.extend_from_slice(dh_i.to_bytes().as_ref()); + } + bytes.extend_from_slice(self.e.to_bytes().as_ref()); + + bytes + } + + pub fn try_from_bytes(bytes: &[u8]) -> Result { + // at the very least we require bytes for: + // - tau_len ( 4 ) + // - tau ( could be 0 for root node ) + // - a ( 48 ) + // - b ( 96 ) + // - length indication of ds ( 4 ) + // - length indication of dh ( 4 ) + // - e ( 96 ) + if bytes.len() < 4 + 48 + 96 + 4 + 4 + 96 { + return Err(DkgError::new_deserialization_failure( + "Node", + "insufficient number of bytes provided", + )); + } + + let tau_len = u32::from_be_bytes((&bytes[..4]).try_into().unwrap()) as usize; + let mut i = 4; + + let tau = Tau::try_from_bytes(&bytes[i..i + tau_len])?; + i += tau_len; + + // perform another length check to account for bytes consumed by tau + if bytes[i..].len() < 48 + 96 + 4 + 4 + 96 { + return Err(DkgError::new_deserialization_failure( + "Node", + "insufficient number of bytes provided", + )); + } + + let a = deserialize_g1(&bytes[i..i + 48]).ok_or_else(|| { + DkgError::new_deserialization_failure("Node.a", "invalid curve point") + })?; + i += 48; + + let b = deserialize_g2(&bytes[i..i + 96]).ok_or_else(|| { + DkgError::new_deserialization_failure("Node.b", "invalid curve point") + })?; + i += 96; + + let ds_len = u32::from_be_bytes((&bytes[i..i + 4]).try_into().unwrap()) as usize; + i += 4; + + if bytes[i..].len() < ds_len * 96 + 4 { + return Err(DkgError::new_deserialization_failure( + "Node", + "insufficient number of bytes provided (ds)", + )); + } + + let mut ds = Vec::with_capacity(ds_len); + for j in 0..ds_len { + let d_i = deserialize_g2(&bytes[i..i + 96]).ok_or_else(|| { + DkgError::new_deserialization_failure( + format!("Node.ds_{}", j), + "invalid curve point", + ) + })?; + + ds.push(d_i); + i += 96; + } + + let dh_len = u32::from_be_bytes((&bytes[i..i + 4]).try_into().unwrap()) as usize; + i += 4; + + if bytes[i..].len() != (dh_len + 1) * 96 { + return Err(DkgError::new_deserialization_failure( + "Node", + "insufficient number of bytes provided (dh)", + )); + } + + let mut dh = Vec::with_capacity(dh_len); + for j in 0..dh_len { + let dh_i = deserialize_g2(&bytes[i..i + 96]).ok_or_else(|| { + DkgError::new_deserialization_failure( + format!("Node.dh_{}", j), + "invalid curve point", + ) + })?; + + dh.push(dh_i); + i += 96; + } + + let e = deserialize_g2(&bytes[i..]).ok_or_else(|| { + DkgError::new_deserialization_failure("Node.h", "invalid curve point") + })?; + + Ok(Node { + tau, + a, + b, + ds, + dh, + e, + }) + } +} + +// produces public key and a decryption key for the root of the tree +pub fn keygen(params: &Params, mut rng: impl RngCore) -> (DecryptionKey, PublicKeyWithProof) { + let g1 = G1Projective::generator(); + let g2 = G2Projective::generator(); + + let mut x = Scalar::random(&mut rng); + let y = g1 * x; + + let proof = ProofOfDiscreteLog::construct(&mut rng, &y, &x); + + let mut rho = Scalar::random(&mut rng); + + let a = g1 * rho; + let b = g2 * x + params.f0 * rho; + + let ds = params.fs.iter().map(|f_i| f_i * rho).collect(); + let dh = params.fh.iter().map(|fh_i| fh_i * rho).collect(); + let e = params.h * rho; + + let dk = DecryptionKey::new_root(Node::new_root(a, b, ds, dh, e)); + + let public_key = PublicKey(y); + let key_with_proof = PublicKeyWithProof { + key: public_key, + proof, + }; + + x.zeroize(); + rho.zeroize(); + + (dk, key_with_proof) +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct PublicKey(pub(crate) G1Projective); + +impl PublicKey { + pub fn verify(&self, proof: &ProofOfDiscreteLog) -> bool { + proof.verify(&self.0) + } +} + +#[derive(Debug)] +#[cfg_attr(test, derive(PartialEq))] +pub struct PublicKeyWithProof { + pub(crate) key: PublicKey, + pub(crate) proof: ProofOfDiscreteLog, +} + +impl PublicKeyWithProof { + pub fn verify(&self) -> bool { + self.key.verify(&self.proof) + } + + pub fn public_key(&self) -> &PublicKey { + &self.key + } + + pub fn to_bytes(&self) -> Vec { + // we have 2 G1 elements and 1 Scalar + let mut bytes = Vec::with_capacity(2 * 48 + 32); + bytes.extend_from_slice(self.key.0.to_bytes().as_ref()); + bytes.extend_from_slice(self.proof.rand_commitment.to_bytes().as_ref()); + bytes.extend_from_slice(self.proof.response.to_bytes().as_ref()); + + bytes + } + + pub fn try_from_bytes(bytes: &[u8]) -> Result { + if bytes.len() != 2 * 48 + 32 { + return Err(DkgError::new_deserialization_failure( + "PublicKeyWithProof", + "provided bytes had invalid length", + )); + } + + let y_bytes = &bytes[..48]; + let commitment_bytes = &bytes[48..96]; + let response_bytes = &bytes[96..]; + + let y = deserialize_g1(y_bytes).ok_or_else(|| { + DkgError::new_deserialization_failure("PublicKeyWithProof.key.0", "invalid curve point") + })?; + + let rand_commitment = deserialize_g1(commitment_bytes).ok_or_else(|| { + DkgError::new_deserialization_failure( + "PublicKeyWithProof.proof.rand_commitment", + "invalid curve point", + ) + })?; + + let response = deserialize_scalar(response_bytes).ok_or_else(|| { + DkgError::new_deserialization_failure( + "PublicKeyWithProof.proof.response", + "invalid scalar", + ) + })?; + + Ok(PublicKeyWithProof { + key: PublicKey(y), + proof: ProofOfDiscreteLog { + rand_commitment, + response, + }, + }) + } +} + +#[derive(Debug, Zeroize)] +#[zeroize(drop)] +#[cfg_attr(test, derive(PartialEq))] +pub struct DecryptionKey { + // note that the nodes are ordered from "right" to "left" + pub(crate) nodes: Vec, +} + +impl DecryptionKey { + fn new_root(root_node: Node) -> Self { + DecryptionKey { + nodes: vec![root_node], + } + } + + fn current(&self) -> Result<&Node, DkgError> { + // we must have at least a single node, otherwise we have a malformed key + self.nodes.last().ok_or(DkgError::MalformedDecryptionKey) + } + + pub fn current_epoch(&self, params: &Params) -> Result, DkgError> { + let current_node = self.current()?; + if current_node.is_root() { + Ok(None) + } else { + Epoch::try_from_tau(¤t_node.tau, params).map(Option::Some) + } + } + + pub(crate) fn try_get_compatible_node(&self, epoch: Epoch) -> Result<&Node, DkgError> { + let tau = epoch.as_tau(); + self.nodes + .iter() + .rev() + .find(|node| node.tau.is_parent_of(&tau)) + .ok_or(DkgError::ExpiredKey) + } + + pub fn try_update_to_next_epoch( + &mut self, + params: &Params, + mut rng: impl RngCore, + ) -> Result<(), DkgError> { + if self.nodes.is_empty() { + return Err(DkgError::MalformedDecryptionKey); + } + + let mut target_epoch = Epoch::new(0); + if self.nodes.len() == 1 && self.nodes[0].is_root() { + return self.try_update_to(target_epoch, params, &mut rng); + } + + // unwrap is fine as we have asserted self.nodes is not empty + self.nodes.pop().unwrap(); + + if let Some(tail) = self.nodes.last() { + target_epoch = tail.tau.lowest_valid_epoch_child(params)?; + } else { + // essentially our key consisted of only a single node and it wasn't a root, + // so either it was malformed or we somehow reached the final epoch and wanted to update + // beyond that. Either way, update to l + 1 is impossible + return Err(DkgError::MalformedDecryptionKey); + } + + self.try_update_to(target_epoch, params, &mut rng) + } + + /// Attempts to update `self` to the provided `epoch`. If the update is not possible, + /// because the target was in the past or the key is malformed, an error is returned. + /// + /// Note that this method mutates the key in place and if the original key was malformed, + /// there are no guarantees about its internal state post-call. + pub fn try_update_to( + &mut self, + target_epoch: Epoch, + params: &Params, + mut rng: impl RngCore, + ) -> Result<(), DkgError> { + if self.nodes.is_empty() { + // somehow we have an empty decryption key + return Err(DkgError::MalformedDecryptionKey); + } + + // makes it easier to work with since we will be generating non-leaf nodes + let target_tau = target_epoch.as_tau(); + let current_tau = &self.current()?.tau; + + if current_tau == &target_tau { + // our key is already updated to the target + return Ok(()); + } + + if current_tau > &target_tau { + // we cannot derive keys for past epochs + return Err(DkgError::TargetEpochUpdateInThePast); + } + + // drop the nodes that are no longer required and get the most direct parent for the target epoch available + let mut parent = loop { + // if pop() fails the key is malformed since we checked that the target_epoch > current_epoch, + // hence the update should have been possible + let tail = self.nodes.pop().ok_or(DkgError::MalformedDecryptionKey)?; + if tail.tau.is_parent_of(&target_tau) { + break tail; + } + }; + + // essentially the case of updating epoch n to n + 1, where n is even; + // in that case the last two nodes are [..., epoch_{n+1}, epoch_n] + // so we just have to reblind the n+1 node and we're done + if parent.tau == target_tau { + parent.reblind(params, &mut rng); + self.nodes.push(parent); + return Ok(()); + } + + // accumulators, note that the previous elements have already been included by the parent, + // i.e. for example for parent at height l <= n, b = g2^x * f0^rho * d1^{tau_1} * ... * dl^{tau_l} + // new_b_accumulator = b * d1^{tau_1} * d2^{tau_2} * ... * dn^{tau_n} + // new_f_accumulator = f0 * f1^{tau_1} * f2^{tau_2} * ... * fn^{tau_n} (up to lambda_t) + let mut new_b_accumulator = parent.b; + let mut new_f_accumulator = parent.tau.evaluate_partial_f(params); + + let parent_height = parent.tau.height(); + + // path from the parent to the child + for (n, bit) in target_tau + .0 + .iter() + .by_vals() + .skip(parent.tau.height()) + .enumerate() + { + // ith bit of the [child] epoch + // note that n represents height difference between parent and the current bit + let i = n + parent_height; + + // if the bit is NOT set, push the right '1' subtree (for future keys) + // so for example if given parent with some `PREFIX` tau and target_epoch being `PREFIX || 010`, + // in the first loop iteration we're going to look at bit `0` and + // derive child node `PREFIX || 1` so that in the future we could derive keys for all other epochs starting with `PREFIX || 1` + // in the next loop iteration we're going to look at bit `1` and simply update the accumulators, + // as we don't need to generate any "left" nodes as all of them would have constructed epochs that are already in the past + // finally, in the last iteration, we look at the bit `0` and derive node `PREFIX || 011`, + // i.e. the one that FOLLOWS the target node. + if !bit { + let direct_parent = target_tau.try_get_parent_at_height(i)?; + + self.nodes + .push(parent.derive_right_nonfinal_child_of_with_partials( + params, + direct_parent, + &new_b_accumulator, + &new_f_accumulator, + &mut rng, + )); + } else { + // only update the accumulators when the bit is set, as d^0 == identity, so there's + // no point in doing anything else; + // note that we don't have to generate any new nodes when going into the right branch + // of the tree as everything on the left would have been in the past, so we don't care about them + new_b_accumulator += parent.ds[n]; // add d0 + new_f_accumulator += params.fs[i]; // f_i + } + } + + self.nodes.push(parent.derive_target_child_with_partials( + params, + target_epoch.as_tau(), + &new_b_accumulator, + &new_f_accumulator, + &mut rng, + )); + + Ok(()) + } + + pub fn to_bytes(&self) -> Vec { + let num_nodes = self.nodes.len() as u32; + + // unfortunately we're not going to know the expected capacity + let mut bytes = Vec::new(); + bytes.extend_from_slice(&num_nodes.to_be_bytes()); + + for node in &self.nodes { + let mut node_bytes = node.to_bytes(); + bytes.extend_from_slice(&((node_bytes.len() as u32).to_be_bytes())); + bytes.append(&mut node_bytes) + } + + bytes + } + + pub fn try_from_bytes(b: &[u8]) -> Result { + // we have to be able to read the length of nodes + if b.len() < 4 { + return Err(DkgError::new_deserialization_failure( + "DecryptionKey", + "insufficient number of bytes provided", + )); + } + let nodes_len = u32::from_be_bytes([b[0], b[1], b[2], b[3]]) as usize; + let mut nodes = Vec::with_capacity(nodes_len); + + let mut i = 4; + for _ in 0..nodes_len { + // check if we can actually read the length... + if b[i..].len() < 4 { + return Err(DkgError::new_deserialization_failure( + "DecryptionKey.Node", + "insufficient number of bytes provided for BTE Node recovery", + )); + } + + let node_bytes = u32::from_be_bytes([b[i], b[i + 1], b[i + 2], b[i + 3]]) as usize; + if b[i + 4..].len() < node_bytes { + return Err(DkgError::new_deserialization_failure( + "DecryptionKey.Node", + "insufficient number of bytes provided for BTE Node recovery", + )); + } + i += 4; + + let node = Node::try_from_bytes(&b[i..i + node_bytes])?; + nodes.push(node); + i += node_bytes; + } + + Ok(DecryptionKey { nodes }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::bte::setup; + use bitvec::bitvec; + use bitvec::order::Msb0; + use rand_core::SeedableRng; + + #[test] + fn basic_coverage_nodes() { + // it's some basic test I've been performing when writing the update function, but figured + // might as well put it into a unit test. note that it doesn't check the entire structure, + // but just the few last nodes of low height + + let params = setup(); + + let dummy_seed = [1u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + + let (mut dk, _) = keygen(¶ms, &mut rng); + + let root_node_copy = dk.nodes.clone(); + + // this is a root node + assert_eq!(dk.nodes.len(), 1); + assert!(dk.nodes[0].is_root()); + + // we have to have a node for right branch on each height (1, 01, 001, ... etc) + // plus an additional one for the two left-most leaves (epochs "0" and "1") + dk.try_update_to(Epoch::new(0), ¶ms, &mut rng).unwrap(); + assert_eq!(dk.nodes.len(), 33); + + let expected_last = Tau::new(0); + // (and yes, I had to look up those names in a thesaurus) + let expected_penultimate = Tau::new(1); + // note that this value is 31bit long + let expected_antepenultimate = Tau(bitvec![u32, Msb0; + 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, 1 + ]); + + let mut nodes_iter = dk.nodes.iter().rev(); + assert_eq!(expected_last, nodes_iter.next().unwrap().tau); + assert_eq!(expected_penultimate, nodes_iter.next().unwrap().tau); + assert_eq!(expected_antepenultimate, nodes_iter.next().unwrap().tau); + + let mut epoch_zero_nodes = dk.nodes.clone(); + + // nodes for epoch1 should be identical for those for epoch0 minus the 00..00 leaf + dk.try_update_to(Epoch::new(1), ¶ms, &mut rng).unwrap(); + assert_eq!(dk.nodes.len(), 32); + epoch_zero_nodes.pop().unwrap(); + assert_eq!( + epoch_zero_nodes + .iter() + .map(|node| node.tau.clone()) + .collect::>(), + dk.nodes + .iter() + .map(|node| node.tau.clone()) + .collect::>() + ); + + dk.try_update_to(Epoch::new(2), ¶ms, &mut rng).unwrap(); + dk.try_update_to(Epoch::new(3), ¶ms, &mut rng).unwrap(); + dk.try_update_to(Epoch::new(4), ¶ms, &mut rng).unwrap(); + + let expected_last = Tau::new(4); + let expected_penultimate = Tau::new(5); + let expected_antepenultimate = Tau(bitvec![u32, Msb0; + 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, 1, 1 + ]); + let expected_preantepenultimate = Tau(bitvec![u32, Msb0; + 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, 1 + ]); + assert_eq!(dk.nodes.len(), 32); + let mut nodes_iter = dk.nodes.iter().rev(); + assert_eq!(expected_last, nodes_iter.next().unwrap().tau); + assert_eq!(expected_penultimate, nodes_iter.next().unwrap().tau); + assert_eq!(expected_antepenultimate, nodes_iter.next().unwrap().tau); + assert_eq!(expected_preantepenultimate, nodes_iter.next().unwrap().tau); + + // the result should be the same of regardless if we update incrementally or go to the target immediately + let mut new_root = DecryptionKey { + nodes: root_node_copy, + }; + new_root + .try_update_to(Epoch::new(4), ¶ms, &mut rng) + .unwrap(); + assert_eq!( + dk.nodes + .iter() + .map(|node| node.tau.clone()) + .collect::>(), + new_root + .nodes + .iter() + .map(|node| node.tau.clone()) + .collect::>() + ); + + // getting expected nodes for those epochs is non-trivial for test purposes, but the last node + // should ALWAYS be equal to the target epoch + dk.try_update_to(Epoch::new(42), ¶ms, &mut rng).unwrap(); + assert_eq!(dk.nodes.last().unwrap().tau, Tau::new(42)); + dk.try_update_to(Epoch::new(123456), ¶ms, &mut rng) + .unwrap(); + assert_eq!(dk.nodes.last().unwrap().tau, Tau::new(123456)); + dk.try_update_to(Epoch::new(3292547435), ¶ms, &mut rng) + .unwrap(); + assert_eq!(dk.nodes.last().unwrap().tau, Tau::new(3292547435)); + + // trying to go to past epochs fails + assert!(dk + .try_update_to(Epoch::new(531), ¶ms, &mut rng) + .is_err()) + } + + #[test] + fn updating_to_next_epoch() { + let params = setup(); + + let dummy_seed = [1u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + + let (mut dk, _) = keygen(¶ms, &mut rng); + + // for root node current epoch is `None` + assert_eq!(None, dk.current_epoch(¶ms).unwrap()); + + // for root node it should result in epoch 0 + dk.try_update_to_next_epoch(¶ms, &mut rng).unwrap(); + assert_eq!(Some(Epoch::new(0)), dk.current_epoch(¶ms).unwrap()); + + dk.try_update_to_next_epoch(¶ms, &mut rng).unwrap(); + assert_eq!(Some(Epoch::new(1)), dk.current_epoch(¶ms).unwrap()); + + dk.try_update_to_next_epoch(¶ms, &mut rng).unwrap(); + assert_eq!(Some(Epoch::new(2)), dk.current_epoch(¶ms).unwrap()); + + // if we start from some non-root epoch, it should result in l + 1 + dk.try_update_to(Epoch::new(42), ¶ms, &mut rng).unwrap(); + dk.try_update_to_next_epoch(¶ms, &mut rng).unwrap(); + assert_eq!(Some(Epoch::new(43)), dk.current_epoch(¶ms).unwrap()); + + dk.try_update_to(Epoch::new(12345), ¶ms, &mut rng) + .unwrap(); + dk.try_update_to_next_epoch(¶ms, &mut rng).unwrap(); + assert_eq!(Some(Epoch::new(12346)), dk.current_epoch(¶ms).unwrap()); + + dk.try_update_to(Epoch::new(3292547435), ¶ms, &mut rng) + .unwrap(); + dk.try_update_to_next_epoch(¶ms, &mut rng).unwrap(); + assert_eq!( + Some(Epoch::new(3292547436)), + dk.current_epoch(¶ms).unwrap() + ); + } + + #[test] + fn public_key_with_proof_roundtrip() { + let params = setup(); + + let dummy_seed = [1u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + + let (_, pk) = keygen(¶ms, &mut rng); + let bytes = pk.to_bytes(); + let recovered = PublicKeyWithProof::try_from_bytes(&bytes).unwrap(); + + assert_eq!(pk, recovered) + } + + #[test] + fn bte_node_roundtrip() { + let params = setup(); + + let dummy_seed = [1u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + + let (mut dk, _) = keygen(¶ms, &mut rng); + + let root_node = dk.nodes[0].clone(); + let bytes = root_node.to_bytes(); + let recovered = Node::try_from_bytes(&bytes).unwrap(); + assert_eq!(root_node, recovered); + + dk.try_update_to(Epoch::new(3292547435), ¶ms, &mut rng) + .unwrap(); + for node in &dk.nodes { + let bytes = node.to_bytes(); + let recovered = Node::try_from_bytes(&bytes).unwrap(); + assert_eq!(node, &recovered); + } + } + + #[test] + fn decryption_key_node_roundtrip() { + let params = setup(); + + let dummy_seed = [1u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + + let (mut dk, _) = keygen(¶ms, &mut rng); + + let bytes = dk.to_bytes(); + let recovered = DecryptionKey::try_from_bytes(&bytes).unwrap(); + assert_eq!(dk, recovered); + + dk.try_update_to(Epoch::new(0), ¶ms, &mut rng).unwrap(); + let bytes = dk.to_bytes(); + let recovered = DecryptionKey::try_from_bytes(&bytes).unwrap(); + assert_eq!(dk, recovered); + + dk.try_update_to(Epoch::new(1), ¶ms, &mut rng).unwrap(); + let bytes = dk.to_bytes(); + let recovered = DecryptionKey::try_from_bytes(&bytes).unwrap(); + assert_eq!(dk, recovered); + + dk.try_update_to(Epoch::new(42), ¶ms, &mut rng).unwrap(); + let bytes = dk.to_bytes(); + let recovered = DecryptionKey::try_from_bytes(&bytes).unwrap(); + assert_eq!(dk, recovered); + + dk.try_update_to(Epoch::new(3292547435), ¶ms, &mut rng) + .unwrap(); + let bytes = dk.to_bytes(); + let recovered = DecryptionKey::try_from_bytes(&bytes).unwrap(); + assert_eq!(dk, recovered); + } +} diff --git a/common/crypto/dkg/src/bte/mod.rs b/common/crypto/dkg/src/bte/mod.rs new file mode 100644 index 0000000000..5e59a8d9d7 --- /dev/null +++ b/common/crypto/dkg/src/bte/mod.rs @@ -0,0 +1,463 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::DkgError; +use crate::utils::{hash_g2, RandomOracleBuilder}; +use crate::{Chunk, Share}; +use bitvec::field::BitField; +use bitvec::order::Msb0; +use bitvec::vec::BitVec; +use bitvec::view::BitView; +use bls12_381::{G1Affine, G1Projective, G2Affine, G2Prepared, G2Projective, Gt}; +use group::Curve; +use lazy_static::lazy_static; +use zeroize::Zeroize; + +pub mod encryption; +pub mod keys; +pub mod proof_chunking; +pub mod proof_discrete_log; +pub mod proof_sharing; + +pub use encryption::{decrypt_share, encrypt_shares, Ciphertexts}; +pub use keys::{keygen, DecryptionKey, PublicKey, PublicKeyWithProof}; + +lazy_static! { + pub(crate) static ref PAIRING_BASE: Gt = + bls12_381::pairing(&G1Affine::generator(), &G2Affine::generator()); + pub(crate) static ref G2_GENERATOR_PREPARED: G2Prepared = + G2Prepared::from(G2Affine::generator()); + pub(crate) static ref DEFAULT_BSGS_TABLE: encryption::BabyStepGiantStepLookup = + encryption::BabyStepGiantStepLookup::default(); +} + +// Domain tries to follow guidelines specified by: +// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#section-3.1 +const SETUP_DOMAIN: &[u8] = b"NYM_COCONUT_NIDKG_V01_CS01_WITH_BLS12381G2_XMD:SHA-256_SSWU_RO_SETUP"; + +// this particular domain is not for curve hashing, but might as well also follow the same naming pattern +const TREE_TAU_EXTENSION_DOMAIN: &[u8] = b"NYM_COCONUT_NIDKG_V01_CS01_SHA-256_TREE_EXTENSION"; + +const MAX_EPOCHS_EXP: usize = 32; +const HASH_SECURITY_PARAM: usize = 256; + +// note: CHUNK_BYTES * NUM_CHUNKS must equal to SCALAR_SIZE +pub const CHUNK_BYTES: usize = 2; +pub const NUM_CHUNKS: usize = 16; +pub const SCALAR_SIZE: usize = 32; + +/// In paper B; number of distinct chunks +pub const CHUNK_SIZE: usize = 1 << (CHUNK_BYTES << 3); + +pub(crate) type EpochStore = u32; + +#[derive(Clone, Debug, PartialEq, PartialOrd)] +// None empty bitvec implies this is a root node +pub(crate) struct Tau(BitVec); + +impl Tau { + pub fn new_root() -> Self { + Tau(BitVec::new()) + } + + // TODO: perhaps this should be explicitly moved to some test module + #[cfg(test)] + pub(crate) fn new(epoch: EpochStore) -> Self { + Tau(epoch.view_bits().to_bitvec()) + } + + #[allow(unused)] + pub fn left_child(&self) -> Self { + let mut child = self.0.clone(); + child.push(false); + Tau(child) + } + + pub fn right_child(&self) -> Self { + let mut child = self.0.clone(); + child.push(true); + Tau(child) + } + + pub fn is_leaf(&self, params: &Params) -> bool { + self.height() == params.lambda_t + } + + pub fn try_get_parent_at_height(&self, height: usize) -> Result { + if height > self.0.len() { + return Err(DkgError::NotAValidParent); + } + + Ok(Tau(self.0[..height].to_bitvec())) + } + + // essentially is this tau prefixing the other + pub fn is_parent_of(&self, other: &Tau) -> bool { + if self.0.len() > other.0.len() { + return false; + } + + for (i, b) in self.0.iter().enumerate() { + if b != other.0[i] { + return false; + } + } + + true + } + + pub fn lowest_valid_epoch_child(&self, params: &Params) -> Result { + if self.0.len() > params.lambda_t { + // this node is already BELOW a valid leaf-epoch node. it can only happen + // if either some invariant was broken or additional data was pushed to `tau` + // in order compute some intermediate results, but in that case this method should have + // never been called anyway. tl;dr: if this is called, the underlying key is malformed + return Err(DkgError::NotAValidParent); + } + let mut child = self.0.clone(); + for _ in 0..(params.lambda_t - self.0.len()) { + child.push(false) + } + + // the unwrap here is fine as we ensure we have exactly `params.tree_height` bits here + // (we could just propagate the error instead of unwraping and putting it behind an `Ok` anyway + // but I'd prefer to just blow up since this would be a serious error + Ok(Epoch::try_from_tau(&Tau(child), params).unwrap()) + } + + pub fn height(&self) -> usize { + self.0.len() + } + + fn extend( + &self, + rr: &[G1Projective; NUM_CHUNKS], + ss: &[G1Projective; NUM_CHUNKS], + cc: &[[G1Projective; NUM_CHUNKS]], + ) -> Self { + let mut random_oracle_builder = RandomOracleBuilder::new(TREE_TAU_EXTENSION_DOMAIN); + random_oracle_builder.update_with_g1_elements(rr.iter()); + random_oracle_builder.update_with_g1_elements(ss.iter()); + for ciphertext_chunks in cc { + random_oracle_builder.update_with_g1_elements(ciphertext_chunks.iter()); + } + + let tau_mem = self.0.as_raw_slice(); + assert_eq!(tau_mem.len(), 1, "tau length invariant was broken"); + random_oracle_builder.update(&tau_mem[0].to_be_bytes()); + + let oracle_output = random_oracle_builder.finalize(); + debug_assert_eq!(oracle_output.len() * 8, HASH_SECURITY_PARAM); + + let mut extended_tau = self.clone(); + for byte in oracle_output { + extended_tau + .0 + .extend_from_bitslice(byte.view_bits::()) + } + + extended_tau + } + + // considers all lambda_t + lambda_h bits + fn evaluate_f(&self, params: &Params) -> G2Projective { + self.0 + .iter() + .by_vals() + .zip(params.fs.iter().chain(params.fh.iter())) + .filter(|(i, _)| *i) + .map(|(_, f_i)| f_i) + .fold(params.f0, |acc, f_i| acc + f_i) + } + + // only considers up to lambda_t bits + fn evaluate_partial_f(&self, params: &Params) -> G2Projective { + self.0 + .iter() + .by_vals() + .zip(params.fs.iter()) + .filter(|(i, _)| *i) + .map(|(_, f_i)| f_i) + .fold(params.f0, |acc, f_i| acc + f_i) + } + + pub(crate) fn to_bytes(&self) -> Vec { + let len_bytes = (self.0.len() as u32).to_be_bytes(); + len_bytes + .into_iter() + .chain(self.0.chunks(8).map(BitField::load_be)) + .collect() + } + + pub(crate) fn try_from_bytes(b: &[u8]) -> Result { + if b.len() < 4 { + return Err(DkgError::new_deserialization_failure( + "Tau", + "insufficient number of bytes provided", + )); + } + let tau_len = u32::from_be_bytes([b[0], b[1], b[2], b[3]]) as usize; + + // maximum theoretical length + if tau_len > MAX_EPOCHS_EXP + HASH_SECURITY_PARAM { + return Err(DkgError::new_deserialization_failure( + "Tau", + format!( + "malformed length {} is greater than maximum {}", + tau_len, + MAX_EPOCHS_EXP + HASH_SECURITY_PARAM + ), + )); + } + + if tau_len == 0 { + if b.len() != 4 { + Err(DkgError::new_deserialization_failure( + "Tau", + "malformed bytes", + )) + } else { + Ok(Tau::new_root()) + } + } else if b.len() == 4 { + Err(DkgError::new_deserialization_failure( + "Tau", + "insufficient number of bytes provided", + )) + } else { + let mut inner = BitVec::repeat(false, tau_len); + for (slot, &byte) in inner.chunks_mut(8).zip(b[4..].iter()) { + slot.store_be(byte); + } + + Ok(Tau(inner)) + } + } +} + +impl Zeroize for Tau { + fn zeroize(&mut self) { + for v in self.0.as_raw_mut_slice() { + v.zeroize() + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)] +pub struct Epoch(EpochStore); + +impl Epoch { + pub fn new(value: EpochStore) -> Self { + Epoch(value) + } + + pub(crate) fn as_tau(&self) -> Tau { + (*self).into() + } + + pub(crate) fn as_extended_tau( + &self, + rr: &[G1Projective; NUM_CHUNKS], + ss: &[G1Projective; NUM_CHUNKS], + cc: &[[G1Projective; NUM_CHUNKS]], + ) -> Tau { + self.as_tau().extend(rr, ss, cc) + } + + pub(crate) fn try_from_tau(tau: &Tau, params: &Params) -> Result { + if !tau.is_leaf(params) { + Err(DkgError::MalformedEpoch) + } else { + Ok(Epoch(tau.0.load_be())) + } + } +} + +impl From for Tau { + fn from(epoch: Epoch) -> Self { + Tau(epoch.0.view_bits().to_bitvec()) + } +} + +impl From for Epoch { + fn from(epoch: EpochStore) -> Self { + Epoch(epoch) + } +} + +pub struct Params { + /// Maximum size of an epoch, in bits. + pub lambda_t: usize, + + /// Security parameter of our $H_{\Lamda_H}$ hash function + pub lambda_h: usize, + + // keeping f0 separate from the rest of the curve points makes it easier to work with tau + f0: G2Projective, + fs: Vec, // f_1, f_2, .... f_{lambda_t} in the paper + fh: Vec, // f_{lambda_t+1}, f_{lambda_t+1}, .... f_{lambda_t+lambda_h} in the paper + h: G2Projective, + + /// Precomputed `h` used for the miller loop + _h_prepared: G2Prepared, +} + +pub fn setup() -> Params { + let f0 = hash_g2(b"f0", SETUP_DOMAIN); + + let fs = (1..=MAX_EPOCHS_EXP) + .map(|i| hash_g2(format!("f{}", i), SETUP_DOMAIN)) + .collect(); + + let fh = (0..HASH_SECURITY_PARAM) + .map(|i| hash_g2(format!("fh{}", i), SETUP_DOMAIN)) + .collect(); + + let h = hash_g2(b"h", SETUP_DOMAIN); + + Params { + lambda_t: MAX_EPOCHS_EXP, + lambda_h: HASH_SECURITY_PARAM, + f0, + fs, + fh, + h, + _h_prepared: G2Prepared::from(h.to_affine()), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use bitvec::bitvec; + use bitvec::order::Msb0; + + #[test] + fn creating_tau_from_epoch() { + assert!(Tau::new_root().0.is_empty()); + + let zero = Tau::new(0); + assert!(zero.0.iter().by_vals().all(|b| !b)); + + let one = Tau::new(1); + let mut iter = one.0.iter().by_vals(); + // first 31 bits are 0, the last one is 1 + for _ in 0..31 { + assert!(!iter.next().unwrap()) + } + assert!(iter.next().unwrap()); + + // 101010 in binary + let forty_two = Tau::new(42); + // first 26 bits are not set + let mut iter = forty_two.0.iter().by_vals(); + for _ in 0..26 { + assert!(!iter.next().unwrap()) + } + assert!(iter.next().unwrap()); + assert!(!iter.next().unwrap()); + assert!(iter.next().unwrap()); + assert!(!iter.next().unwrap()); + assert!(iter.next().unwrap()); + assert!(!iter.next().unwrap()); + + // value that requires an actual u32 (i.e. takes 4 bytes to represent) + // 11000100_01000000_01001001_01101011 in binary + let big_val = Tau::new(3292547435); + let expected = bitvec![u32, Msb0; + 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, + 0, 1, 1 + ]; + assert_eq!(expected, big_val.0) + } + + #[test] + fn getting_parent_at_height() { + let tau = Tau(bitvec![u32, Msb0; 1,0,1,1,0,0,1]); + + let expected_0 = Tau(BitVec::new()); + let expected_1 = Tau(bitvec![u32, Msb0; 1]); + let expected_5 = Tau(bitvec![u32, Msb0; 1,0,1,1,0]); + + assert_eq!(expected_0, tau.try_get_parent_at_height(0).unwrap()); + assert_eq!(expected_1, tau.try_get_parent_at_height(1).unwrap()); + assert_eq!(expected_5, tau.try_get_parent_at_height(5).unwrap()); + assert_eq!(tau, tau.try_get_parent_at_height(7).unwrap()); + assert!(tau.try_get_parent_at_height(8).is_err()) + } + + #[test] + fn converting_tau_to_epoch() { + let params = setup(); + + let tau0: Tau = Epoch::new(0).into(); + let tau1: Tau = Epoch::new(1).into(); + let tau42: Tau = Epoch::new(42).into(); + let tau_big: Tau = Epoch::new(3292547435).into(); + + assert_eq!(Epoch::new(0), Epoch::try_from_tau(&tau0, ¶ms).unwrap()); + assert_eq!(Epoch::new(1), Epoch::try_from_tau(&tau1, ¶ms).unwrap()); + assert_eq!( + Epoch::new(42), + Epoch::try_from_tau(&tau42, ¶ms).unwrap() + ); + assert_eq!( + Epoch::new(3292547435), + Epoch::try_from_tau(&tau_big, ¶ms).unwrap() + ); + + assert!(Epoch::try_from_tau(&Tau(BitVec::new()), ¶ms).is_err()); + assert!(Epoch::try_from_tau(&Tau(bitvec![u32, Msb0; 1,0,1,1,0]), ¶ms).is_err()); + let _31bit_tau = Tau(bitvec![u32, Msb0; + 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, + 0, 1 + ]); + assert!(Epoch::try_from_tau(&_31bit_tau, ¶ms).is_err()); + + let _33bit_tau = Tau(bitvec![u32, Msb0; + 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, + 0, 1, 1, 0 + ]); + assert!(Epoch::try_from_tau(&_33bit_tau, ¶ms).is_err()); + } + + #[test] + fn tau_roundtrip() { + let good_taus = vec![ + Tau::new_root(), + Tau::new(0), + Tau::new(1), + Tau::new(2), + Tau::new(42), + Tau::new(123456), + Tau::new(3292547435), + Tau::new(u32::MAX), + ]; + + for tau in good_taus { + let bytes = tau.to_bytes(); + let recovered = Tau::try_from_bytes(&bytes).unwrap(); + assert_eq!(tau, recovered); + } + + // more valid variants + let mut another_tau = Tau::new(u32::MAX); + another_tau.0.push(true); + another_tau.0.push(false); + another_tau.0.push(true); + + let bytes = another_tau.to_bytes(); + let recovered = Tau::try_from_bytes(&bytes).unwrap(); + assert_eq!(another_tau, recovered); + + // ensure there are no panics + let big_length_bytes = [255, 255, 255, 255, 42]; + assert!(Tau::try_from_bytes(&big_length_bytes).is_err()); + + assert!(Tau::try_from_bytes(&[]).is_err()); + assert!(Tau::try_from_bytes(&[1, 1, 1, 1]).is_err()); + assert!(Tau::try_from_bytes(&[0, 0, 0, 1]).is_err()); + assert!(Tau::try_from_bytes(&[1, 0, 0, 0]).is_err()); + assert!(Tau::try_from_bytes(&[1, 0, 0]).is_err()); + } +} diff --git a/common/crypto/dkg/src/bte/proof_chunking.rs b/common/crypto/dkg/src/bte/proof_chunking.rs new file mode 100644 index 0000000000..ad58fee2d5 --- /dev/null +++ b/common/crypto/dkg/src/bte/proof_chunking.rs @@ -0,0 +1,1079 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::bte::encryption::Ciphertexts; +use crate::bte::{Chunk, PublicKey, Share, CHUNK_SIZE, NUM_CHUNKS}; +use crate::ensure_len; +use crate::error::DkgError; +use crate::utils::{deserialize_g1, hash_to_scalar}; +use crate::utils::{deserialize_scalar, RandomOracleBuilder}; +use bls12_381::{G1Projective, Scalar}; +use ff::Field; +use group::{Group, GroupEncoding}; +use rand::Rng; +use rand_core::{RngCore, SeedableRng}; + +const CHUNKING_ORACLE_DOMAIN: &[u8] = + b"NYM_COCONUT_NIDKG_V01_CS01_SHA-256_CHACHA20_CHUNKING_ORACLE"; + +const SECOND_CHALLENGE_DOMAIN: &[u8] = + b"NYM_COCONUT_NIDKG_V01_CS01_WITH_BLS12381_XMD:SHA-256_SSWU_RO_PROOF_CHUNKING_SECOND_CHALLENGE"; + +/// The number of parallel runs batched in a single challenge, +/// `l` ($\ell$) in the DKG paper. +const PARALLEL_RUNS: usize = 32; + +/// `lambda` ($\lambda$) in the DKG paper +const SECURITY_PARAMETER: usize = 256; + +// note: ceiling in integer division can be achieved via q = (x + y - 1) / y; +/// ceil(SECURITY_PARAMETER / PARALLEL_RUNS) in the paper +const NUM_CHALLENGE_BITS: usize = (SECURITY_PARAMETER + PARALLEL_RUNS - 1) / PARALLEL_RUNS; + +// type alias for ease of use +type FirstChallenge = Vec>>; + +#[cfg_attr(test, derive(Clone))] +pub struct Instance<'a> { + /// y_1, ..., y_n + public_keys: &'a [PublicKey], + + /// R_1, ..., R_m + rr: &'a [G1Projective; NUM_CHUNKS], + + /// C_{1,1}, ..., C_{n,m} + ciphertext_chunks: &'a [[G1Projective; NUM_CHUNKS]], +} + +impl<'a> Instance<'a> { + pub fn new(public_keys: &'a [PublicKey], ciphertext: &'a Ciphertexts) -> Instance<'a> { + Instance { + public_keys, + rr: &ciphertext.rr, + ciphertext_chunks: &ciphertext.ciphertext_chunks, + } + } + + fn validate(&self) -> bool { + if self.public_keys.is_empty() { + return false; + } + + if self.public_keys.len() != self.ciphertext_chunks.len() { + return false; + } + + true + } +} + +#[derive(Debug)] +#[cfg_attr(test, derive(Clone, PartialEq))] +pub struct ProofOfChunking { + y0: G1Projective, + bb: Vec, + cc: Vec, + dd: Vec, + yy: G1Projective, + responses_r: Vec, + responses_chunks: Vec, + response_beta: Scalar, +} + +impl ProofOfChunking { + // Some decisions made in this function code can be questionable, however, I will attempt to justify them. + // I decided to operate on u64 when constructing responses for the chunks rather than using + // `Scalar` directly that could have future-proofed everything in case we decided to significantly + // increase our chunk sizes alongside PARALLEL_RUNS, number of nodes, etc. such that it would no longer + // fit within u64. However, this is not very likely and runtime checks are in place to ensure it hasn't happened. + // + // As for the actual issue, at the time of writing this code, there is no way of ordering two Scalars. + // And conceptually it makes sense as ordering of scalars in finite fields doesn't make much + // sense mathematically. Anyway, in my original proof of concept code I had to compare byte representations + // of the scalars. While in principle it still makes sense (assuming they're in the canonical representations), + // we run into a problem if say we wanted to compare Scalar(1) with Scalar(-1). Logically the -1 variant + // should have been smaller. However, internally everything is represented mod field order and thus + // Scalar(-1) would in reality be Scalar(q - 1), which is greater than Scalar(1) and opposite to + // what we wanted. + pub fn construct( + mut rng: impl RngCore, + instance: Instance, + witness_r: &[Scalar; NUM_CHUNKS], + witnesses_s: &[Share], + ) -> Result { + if !instance.validate() { + return Err(DkgError::MalformedProofOfChunkingInstance); + } + + let g1 = G1Projective::generator(); + + let y0 = G1Projective::random(&mut rng); + + // define bounds for the blinding factors + let n = instance.public_keys.len(); + let m = NUM_CHUNKS; + let ee = 1 << NUM_CHALLENGE_BITS; + + // CHUNK_MAX corresponds to paper's B + let ss = (n * m * (CHUNK_SIZE - 1) * (ee - 1)) as u64; + let zz = (2 * (PARALLEL_RUNS as u64)) + .checked_mul(ss) + .expect("overflow in Z = 2 * l * S"); + + let ss_scalar = Scalar::from(ss); + + // rather than generating blinding factors in [-S, Z-1] directly, + // do it via [0, Z - 1 + S + 1] and deal with the shift later. + let combined_upper_range = (zz - 1) + .checked_add(ss + 1) + .expect("overflow in Z - 1 + S + 1"); + + let mut betas = Vec::with_capacity(PARALLEL_RUNS); + let mut bs = Vec::with_capacity(PARALLEL_RUNS); + + for _ in 0..PARALLEL_RUNS { + let beta = Scalar::random(&mut rng); + + // g1 ^ beta_l + let bb = g1 * beta; + + betas.push(beta); + bs.push(bb); + } + + let mut attempt = 0; + let (first_challenge, responses_chunks, cs) = 'retry_loop: loop { + attempt += 1; + if attempt == SECURITY_PARAMETER { + return Err(DkgError::AbortedProofOfChunking); + } + + // let mut blinding_factors = Vec::with_capacity(PARALLEL_RUNS); + let mut shifted_blinding_factors = Vec::with_capacity(PARALLEL_RUNS); + let mut cs = Vec::with_capacity(PARALLEL_RUNS); + + // I think this part is more readable with a range loop + #[allow(clippy::needless_range_loop)] + for i in 0..PARALLEL_RUNS { + // scalar in range of [0, Z - 1 + S] + let shifted = rng.gen_range(0..=combined_upper_range); + // [-S, Z - 1] as required + let blinding_factor = Scalar::from(shifted) - ss_scalar; + + // y0 ^ beta_l • g1 ^ sigma_l + let cc = y0 * betas[i] + g1 * blinding_factor; + + // blinding_factors.push(blinding_factor); + shifted_blinding_factors.push(shifted); + cs.push(cc); + } + + let first_challenge = Self::compute_first_challenge(&instance, &y0, &bs, &cs, n, m); + + // compute: + // z_{s,1} = sum(i <- 1..n; (sum j <- j..m; e_{i,j,1} • s_{i,j} + sigma_1)) + // ... + // z_{s,l} = sum(i <- 1..n; (sum j <- j..m; e_{i,j,l} • s_{i,j} + sigma_l)) + // such that 0 <= z_{s,l} < Z + let mut responses_chunks = Vec::with_capacity(PARALLEL_RUNS); + + // I think this part is more readable with a range loop + #[allow(clippy::needless_range_loop)] + for l in 0..PARALLEL_RUNS { + let mut sum = 0; + + for (i, witness_i) in witnesses_s.iter().enumerate() { + for (j, witness_ij) in witness_i.to_chunks().chunks.iter().enumerate() { + debug_assert!(std::mem::size_of::() <= std::mem::size_of::()); + sum += first_challenge[i][j][l] * (*witness_ij as u64) + } + } + + if sum + shifted_blinding_factors[l] < ss { + continue 'retry_loop; + } + // shifted_blinding_factors[l] - ss restores it to "proper" [-S, Z - 1] range + let response = sum + shifted_blinding_factors[l] - ss; + if response < zz { + responses_chunks.push(response) + } else { + continue 'retry_loop; + } + } + + break (first_challenge, responses_chunks, cs); + }; + + let mut deltas = Vec::with_capacity(n + 1); + let mut ds = Vec::with_capacity(n + 1); + + for _ in 0..=n { + let delta = Scalar::random(&mut rng); + let dd = g1 * delta; + + deltas.push(delta); + ds.push(dd); + } + + // Y = y_0 ^ delta_0 • y_1 ^ delta_1 • ... • y_n ^ delta_n + let mut yy = y0 * deltas[0]; + for (i, delta_i) in deltas.iter().enumerate().skip(1) { + yy += instance.public_keys[i - 1].0 * delta_i; + } + + let second_challenge = + Self::compute_second_challenge(&first_challenge, &responses_chunks, &ds, &yy); + + // compute responses + + let mut responses_r = Vec::with_capacity(n); + + for (i, e_i) in first_challenge.iter().enumerate() { + let mut response_r_k = deltas[i + 1]; + for (j, e_ij) in e_i.iter().enumerate() { + // c^1 in first iteration, c^k in the last + let mut challenge_pow = second_challenge; + for e_ijk in e_ij.iter() { + response_r_k += Scalar::from(*e_ijk) * witness_r[j] * challenge_pow; + challenge_pow *= second_challenge + } + } + + responses_r.push(response_r_k); + } + + let mut response_beta = deltas[0]; + // c^1 in first iteration, c^k in the last + let mut challenge_pow = second_challenge; + for beta_k in betas { + // r_beta = beta_1 • challenge^1 + beta_2 • challenge^2 + ... beta_k • challenge^k + delta_0 + response_beta += beta_k * challenge_pow; + challenge_pow *= second_challenge + } + + Ok(ProofOfChunking { + y0, + bb: bs, + cc: cs, + dd: ds, + yy, + responses_r, + responses_chunks, + response_beta, + }) + } + + pub fn verify(&self, instance: Instance) -> bool { + if !instance.validate() { + return false; + } + + let g1 = G1Projective::generator(); + let n = instance.public_keys.len(); + let m = instance.rr.len(); + + ensure_len!(&self.bb, PARALLEL_RUNS); + ensure_len!(&self.cc, PARALLEL_RUNS); + ensure_len!(&self.dd, n + 1); + ensure_len!(&self.responses_r, n); + ensure_len!(&self.responses_chunks, PARALLEL_RUNS); + + let ee = 1 << NUM_CHALLENGE_BITS; + + // CHUNK_MAX corresponds to paper's B + let ss = (n * m * (CHUNK_SIZE - 1) * (ee - 1)) as u64; + let zz = 2 * (PARALLEL_RUNS as u64) * ss; + + for response_chunk in &self.responses_chunks { + if response_chunk >= &zz { + return false; + } + } + + let first_challenge = + Self::compute_first_challenge(&instance, &self.y0, &self.bb, &self.cc, n, m); + + let second_challenge = Self::compute_second_challenge( + &first_challenge, + &self.responses_chunks, + &self.dd, + &self.yy, + ); + + // memoise chlg^k for k in 1..l since they're used in multiple checks, so there's + // no point in recomputing them every time + let mut challenge_pows = Vec::with_capacity(PARALLEL_RUNS); + challenge_pows.push(second_challenge); + for k in 1..PARALLEL_RUNS { + challenge_pows.push(challenge_pows[k - 1] * second_challenge) + } + + // for i in [1..n] check if: + // R_1 ^ (e_{i,1,1} * chlg^1 + ... + e_{i,1,l} * chlg^l) • ... • R_m ^ (e_{i,m,1} * chlg^1 + ... + e_{i,m,l} * chlg^l) • D_i + // == + // g1^reponses_r_i + for (i, response_r_i) in self.responses_r.iter().enumerate() { + // rhs = D_i + let mut product = self.dd[i + 1]; + for (j, e_ij) in first_challenge[i].iter().enumerate() { + // intermediate (e_{i,j,1} * chlg^1 + ... + e_{i,j,l} * chlg^l) sum + let mut sum = Scalar::zero(); + for (k, e_ijk) in e_ij.iter().enumerate() { + sum += Scalar::from(*e_ijk) * challenge_pows[k] + } + // for j in [1..m] + // rhs = D_i • R_1 ^ sum_1 • ... • R_m ^ sum_m + product += instance.rr[j] * sum + } + + if product != g1 * response_r_i { + return false; + } + } + + // check if B_{1}^{chlg^1} • ... • B_{l}^{chlg^l} • D_0 == g1^{response_beta} + let mut product = self.dd[0]; + for (k, b_k) in self.bb.iter().enumerate() { + product += b_k * challenge_pows[k] + } + + if product != g1 * self.response_beta { + return false; + } + + // check if + // (C_{1,1} ^ e_{1,1,1} • ... • C_{n,m} ^ e_{n,m,1}) ^ chlg^1 • ... (C_{1,1} ^ e_{1,1,l} • ... • C_{n,m} ^ e_{n,m,l}) ^ chlg^l • Y + // == + // pk_1 ^ responses_r_1 • ... • pk_n ^ responses_r_n • y_0^{response_beta} • g1^{response_chunks_1 • chlg^1 + ... + response_chunks_l • chlg^l} + + let mut lhs = self.yy; + + // compute product (C_{1,1} ^ e_{1,1,1} • ... • C_{n,m} ^ e_{n,m,1}) ^ chlg^1 • ... (C_{1,1} ^ e_{1,1,l} • ... • C_{n,m} ^ e_{n,m,l}) ^ chlg^l + // I think this part is more readable with a range loop + #[allow(clippy::needless_range_loop)] + for k in 0..PARALLEL_RUNS { + let mut inner_acc = G1Projective::identity(); + for (i, c_i) in instance.ciphertext_chunks.iter().enumerate() { + for (j, c_ij) in c_i.iter().enumerate() { + inner_acc += c_ij * Scalar::from(first_challenge[i][j][k]); + } + } + // TODO: can this be simplified? + inner_acc *= challenge_pows[k]; + lhs += inner_acc + } + + // finally multiply by C_{1}^{chlg^1} • ... • C_{l}^{chlg^l} + for (k, c) in self.cc.iter().enumerate() { + lhs += c * challenge_pows[k] + } + + // calculate intermediate product pk_1 ^ responses_r_1 • ... • pk_n ^ responses_r_n + let mut product = G1Projective::identity(); + for (i, pk) in instance.public_keys.iter().enumerate() { + product += pk.0 * self.responses_r[i] + } + + // calculate intermediate sum response_chunks_1 • chlg^1 + ... + response_chunks_l • chlg^l + let mut sum = Scalar::zero(); + for (k, response_chunk) in self.responses_chunks.iter().enumerate() { + sum += Scalar::from(*response_chunk) * challenge_pows[k] + } + + let rhs = product + self.y0 * self.response_beta + g1 * sum; + + if lhs != rhs { + return false; + } + + true + } + + fn compute_first_challenge( + instance: &Instance, + y0: &G1Projective, + bb: &[G1Projective], + cc: &[G1Projective], + n: usize, + m: usize, + ) -> FirstChallenge { + // lambda_e = n • m • l • ceil(lambda / l) + let lambda_e = n * m * PARALLEL_RUNS * NUM_CHALLENGE_BITS; + + let mut random_oracle_builder = RandomOracleBuilder::new(CHUNKING_ORACLE_DOMAIN); + random_oracle_builder.update_with_g1_elements(instance.public_keys.iter().map(|pk| &pk.0)); + random_oracle_builder.update_with_g1_elements(instance.rr.iter()); + instance + .ciphertext_chunks + .iter() + .for_each(|chunks| random_oracle_builder.update_with_g1_elements(chunks.iter())); + random_oracle_builder.update(y0.to_bytes()); + random_oracle_builder.update_with_g1_elements(bb.iter()); + random_oracle_builder.update_with_g1_elements(cc.iter()); + random_oracle_builder.update(lambda_e.to_be_bytes()); + + let mut oracle = rand_chacha::ChaCha20Rng::from_seed(random_oracle_builder.finalize()); + let range_max_excl = 1 << NUM_CHALLENGE_BITS; + + (0..n) + .map(|_| { + (0..m) + .map(|_| { + (0..PARALLEL_RUNS) + .map(|_| oracle.gen_range(0..range_max_excl)) + .collect() + }) + .collect() + }) + .collect() + } + + fn compute_second_challenge( + first_challenge: &FirstChallenge, + responses_chunks: &[u64], + ds: &[G1Projective], + y: &G1Projective, + ) -> Scalar { + let scalars = + first_challenge.len() * first_challenge[0].len() * first_challenge[0][0].len() + + responses_chunks.len(); + let g1s = ds.len() + 1; + + let mut bytes = Vec::with_capacity(scalars * 32 + g1s * 48); + + for e_i in first_challenge { + for e_ij in e_i { + for e_ijk in e_ij { + bytes.extend_from_slice(e_ijk.to_be_bytes().as_ref()); + } + } + } + + for z in responses_chunks { + bytes.extend_from_slice(z.to_be_bytes().as_ref()) + } + + for d in ds { + bytes.extend_from_slice(d.to_bytes().as_ref()) + } + + bytes.extend_from_slice(y.to_bytes().as_ref()); + + hash_to_scalar(bytes, SECOND_CHALLENGE_DOMAIN) + } + + pub(crate) fn to_bytes(&self) -> Vec { + let g1s = self.bb.len() + self.cc.len() + self.dd.len() + 2; + let scalars = self.responses_r.len() + 1; + let u64s = self.responses_chunks.len(); + + // we also need length indicators for bb, cc, dd, responses_r and responses_chunks + let mut bytes = Vec::with_capacity(g1s * 48 + scalars * 32 + u64s * 8 + 5 * 4); + bytes.extend_from_slice(self.y0.to_bytes().as_ref()); + + bytes.extend_from_slice(&(self.bb.len() as u32).to_be_bytes()); + for b in &self.bb { + bytes.extend_from_slice(b.to_bytes().as_ref()); + } + + bytes.extend_from_slice(&(self.cc.len() as u32).to_be_bytes()); + for c in &self.cc { + bytes.extend_from_slice(c.to_bytes().as_ref()); + } + + bytes.extend_from_slice(&(self.dd.len() as u32).to_be_bytes()); + for d in &self.dd { + bytes.extend_from_slice(d.to_bytes().as_ref()); + } + + bytes.extend_from_slice(self.yy.to_bytes().as_ref()); + + bytes.extend_from_slice(&(self.responses_r.len() as u32).to_be_bytes()); + for rr in &self.responses_r { + bytes.extend_from_slice(rr.to_bytes().as_ref()); + } + + bytes.extend_from_slice(&(self.responses_chunks.len() as u32).to_be_bytes()); + for rc in &self.responses_chunks { + bytes.extend_from_slice(rc.to_be_bytes().as_ref()); + } + + bytes.extend_from_slice(self.response_beta.to_bytes().as_ref()); + + bytes + } + + pub(crate) fn try_from_bytes(bytes: &[u8]) -> Result { + // determining the minimum number of bytes is tricky, so we'll be checking if we have enough as we go + + // can we read y0 and length of bb? + if bytes.len() < 48 + 4 { + return Err(DkgError::new_deserialization_failure( + "ProofOfChunking", + "insufficient number of bytes provided", + )); + } + + let mut i = 0; + let y0 = deserialize_g1(&bytes[i..i + 48]).ok_or_else(|| { + DkgError::new_deserialization_failure("ProofOfChunking.y0", "invalid curve point") + })?; + i += 48; + + let bb_len = u32::from_be_bytes((&bytes[i..i + 4]).try_into().unwrap()) as usize; + i += 4; + + // can we read bb and length of cc? + if bytes[i..].len() < 48 * bb_len + 4 { + return Err(DkgError::new_deserialization_failure( + "ProofOfChunking", + "insufficient number of bytes provided", + )); + } + + let mut bb = Vec::with_capacity(bb_len); + for _ in 0..bb_len { + bb.push(deserialize_g1(&bytes[i..i + 48]).ok_or_else(|| { + DkgError::new_deserialization_failure("ProofOfChunking.bb", "invalid curve point") + })?); + i += 48; + } + + let cc_len = u32::from_be_bytes((&bytes[i..i + 4]).try_into().unwrap()) as usize; + i += 4; + + // can we read cc and length of dd? + if bytes[i..].len() < 48 * cc_len + 4 { + return Err(DkgError::new_deserialization_failure( + "ProofOfChunking", + "insufficient number of bytes provided", + )); + } + + let mut cc = Vec::with_capacity(cc_len); + for _ in 0..cc_len { + cc.push(deserialize_g1(&bytes[i..i + 48]).ok_or_else(|| { + DkgError::new_deserialization_failure("ProofOfChunking.cc", "invalid curve point") + })?); + i += 48; + } + + let dd_len = u32::from_be_bytes((&bytes[i..i + 4]).try_into().unwrap()) as usize; + i += 4; + + // can we read dd, yy and length of responses_r? + if bytes[i..].len() < 48 * dd_len + 48 + 4 { + return Err(DkgError::new_deserialization_failure( + "ProofOfChunking", + "insufficient number of bytes provided", + )); + } + + let mut dd = Vec::with_capacity(dd_len); + for _ in 0..dd_len { + dd.push(deserialize_g1(&bytes[i..i + 48]).ok_or_else(|| { + DkgError::new_deserialization_failure("ProofOfChunking.dd", "invalid curve point") + })?); + i += 48; + } + + let yy = deserialize_g1(&bytes[i..i + 48]).ok_or_else(|| { + DkgError::new_deserialization_failure("ProofOfChunking.y0", "invalid curve point") + })?; + i += 48; + + let responses_r_len = u32::from_be_bytes((&bytes[i..i + 4]).try_into().unwrap()) as usize; + i += 4; + + // can we read responses_r and length of responses_chunks? + if bytes[i..].len() < 32 * responses_r_len + 4 { + return Err(DkgError::new_deserialization_failure( + "ProofOfChunking", + "insufficient number of bytes provided", + )); + } + + let mut responses_r = Vec::with_capacity(responses_r_len); + for _ in 0..responses_r_len { + responses_r.push(deserialize_scalar(&bytes[i..i + 32]).ok_or_else(|| { + DkgError::new_deserialization_failure( + "ProofOfChunking.responses_r", + "invalid scalar", + ) + })?); + i += 32; + } + + let responses_chunks_len = + u32::from_be_bytes((&bytes[i..i + 4]).try_into().unwrap()) as usize; + i += 4; + + // can we read the rest of the proof, i.e. responses_chunks and response_beta? + if bytes[i..].len() != responses_chunks_len * 8 + 32 { + return Err(DkgError::new_deserialization_failure( + "ProofOfChunking", + "invalid number of bytes provided", + )); + } + + let mut responses_chunks = Vec::with_capacity(responses_chunks_len); + for _ in 0..responses_chunks_len { + responses_chunks.push(u64::from_be_bytes((&bytes[i..i + 8]).try_into().unwrap())); + i += 8; + } + + let response_beta = deserialize_scalar(&bytes[i..i + 32]).ok_or_else(|| { + DkgError::new_deserialization_failure("ProofOfChunking.response_beta", "invalid scalar") + })?; + + Ok(ProofOfChunking { + y0, + bb, + cc, + dd, + yy, + responses_r, + responses_chunks, + response_beta, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::bte::Share; + use crate::ChunkedShare; + + // limit number of nodes to some reasonable-ish value, as it significantly affects + // time it takes to compute and verify the proof + const NODES: usize = 20; + + struct OwnedInstance { + public_keys: Vec, + randomizers_r: [G1Projective; NUM_CHUNKS], + ciphertext_chunks: Vec<[G1Projective; NUM_CHUNKS]>, + } + + fn setup(mut rng: impl RngCore) -> (OwnedInstance, [Scalar; NUM_CHUNKS], Vec) { + let g1 = G1Projective::generator(); + + let mut pks = Vec::with_capacity(NODES); + + for _ in 0..NODES { + pks.push(PublicKey(g1 * Scalar::random(&mut rng))); + } + + let mut r = Vec::with_capacity(NUM_CHUNKS); + let mut rr = Vec::with_capacity(NUM_CHUNKS); + + for _ in 0..NUM_CHUNKS { + let r_i = Scalar::random(&mut rng); + rr.push(g1 * r_i); + r.push(r_i); + } + + let mut ciphertext_chunks = Vec::with_capacity(NODES); + let mut shares = Vec::with_capacity(NODES); + + for pk_i in &pks { + let share = Share::random(&mut rng); + + let mut ciphertext_chunk_i = Vec::with_capacity(NUM_CHUNKS); + for (j, chunk) in share.to_chunks().chunks.iter().enumerate() { + let c = pk_i.0 * r[j] + g1 * Scalar::from(*chunk as u64); + ciphertext_chunk_i.push(c) + } + + ciphertext_chunks.push(ciphertext_chunk_i.try_into().unwrap()); + shares.push(share); + } + + ( + OwnedInstance { + public_keys: pks, + randomizers_r: rr.try_into().unwrap(), + ciphertext_chunks, + }, + r.try_into().unwrap(), + shares, + ) + } + + #[test] + fn should_fail_to_create_proof_with_invalid_instance() { + let dummy_seed = [1u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + + let (owned_instance, _, _) = setup(&mut rng); + let good_instance = Instance { + public_keys: &owned_instance.public_keys, + rr: &owned_instance.randomizers_r, + ciphertext_chunks: &owned_instance.ciphertext_chunks, + }; + + // sanity check + assert!(good_instance.validate()); + + // no keys + let mut bad_instance = good_instance.clone(); + bad_instance.public_keys = &[]; + assert!(!bad_instance.validate()); + + // too many keys + let mut bad_keys = owned_instance.public_keys.clone(); + bad_keys.push(PublicKey( + G1Projective::generator() * Scalar::random(&mut rng), + )); + + let mut bad_instance = good_instance.clone(); + bad_instance.public_keys = &bad_keys; + assert!(!bad_instance.validate()); + + // too few keys + let mut bad_keys = owned_instance.public_keys.clone(); + bad_keys.truncate(bad_keys.len() - 1); + + let mut bad_instance = good_instance.clone(); + bad_instance.public_keys = &bad_keys; + assert!(!bad_instance.validate()); + + // no ciphertexts + let mut bad_instance = good_instance.clone(); + bad_instance.ciphertext_chunks = &[]; + assert!(!bad_instance.validate()); + + // too many ciphertexts + let mut bad_ciphertexts = owned_instance.ciphertext_chunks.clone(); + bad_ciphertexts.push(Default::default()); + + let mut bad_instance = good_instance.clone(); + bad_instance.ciphertext_chunks = &bad_ciphertexts; + assert!(!bad_instance.validate()); + + // too few ciphertexts + let mut bad_ciphertexts = owned_instance.ciphertext_chunks.clone(); + bad_ciphertexts.truncate(bad_ciphertexts.len() - 1); + + let mut bad_instance = good_instance.clone(); + bad_instance.ciphertext_chunks = &bad_ciphertexts; + assert!(!bad_instance.validate()); + } + + #[test] + fn should_verify_a_valid_proof() { + let dummy_seed = [1u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + + let (owned_instance, r, shares) = setup(&mut rng); + + let instance = Instance { + public_keys: &owned_instance.public_keys, + rr: &owned_instance.randomizers_r, + ciphertext_chunks: &owned_instance.ciphertext_chunks, + }; + + let chunking_proof = + ProofOfChunking::construct(&mut rng, instance.clone(), &r, &shares).unwrap(); + + assert!(chunking_proof.verify(instance)) + } + + #[test] + fn works_with_chunks_of_extreme_sizes() { + // Note: by extreme I mean CHUNK_MAX or 0 + let dummy_seed = [1u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + + let g1 = G1Projective::generator(); + + let mut pks = Vec::with_capacity(3); + + for _ in 0..3 { + pks.push(PublicKey(g1 * Scalar::random(&mut rng))); + } + + let mut r = Vec::with_capacity(NUM_CHUNKS); + let mut rr = Vec::with_capacity(NUM_CHUNKS); + + for _ in 0..NUM_CHUNKS { + let r_i = Scalar::random(&mut rng); + rr.push(g1 * r_i); + r.push(r_i); + } + + let mut ciphertext_chunks = Vec::with_capacity(3); + + let share1 = Share(Scalar::zero()); + let share2 = Share(Scalar::one()); + + let chunks1 = share1.to_chunks(); + let chunks2 = share2.to_chunks(); + + // note that we can't have just [Chunk::MAX; NUM_CHUNKS] as it cannot be converted + // back to scalar since its byte representation would be just 1s and it's not a canonical + // Scalar reduced mod q + let chunks3 = ChunkedShare { + chunks: [ + 1, + Chunk::MAX, + 3, + 4, + Chunk::MAX, + Chunk::MAX - 1, + 0, + 0, + 0, + 0, + 42, + 0, + 0, + Chunk::MAX, + 0, + 0, + ], + }; + + let share3 = chunks3.clone().try_into().unwrap(); + + let shares = vec![share1, share2, share3]; + let chunks = vec![chunks1, chunks2, chunks3]; + + for (i, pk_i) in pks.iter().enumerate() { + let mut ciphertext_chunk_i = Vec::with_capacity(NUM_CHUNKS); + for (j, chunk) in chunks[i].chunks.iter().enumerate() { + let c = pk_i.0 * r[j] + g1 * Scalar::from(*chunk as u64); + ciphertext_chunk_i.push(c) + } + + ciphertext_chunks.push(ciphertext_chunk_i.try_into().unwrap()); + } + + let randomizers_r = rr.try_into().unwrap(); + let witness_r = r.try_into().unwrap(); + + let instance = Instance { + public_keys: &pks, + rr: &randomizers_r, + ciphertext_chunks: &ciphertext_chunks, + }; + + let chunking_proof = + ProofOfChunking::construct(&mut rng, instance.clone(), &witness_r, &shares).unwrap(); + + assert!(chunking_proof.verify(instance)) + } + + #[test] + fn should_fail_to_verify_proof_with_invalid_instance() { + let dummy_seed = [1u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + + let (owned_instance, r, shares) = setup(&mut rng); + let good_instance = Instance { + public_keys: &owned_instance.public_keys, + rr: &owned_instance.randomizers_r, + ciphertext_chunks: &owned_instance.ciphertext_chunks, + }; + + let chunking_proof = + ProofOfChunking::construct(&mut rng, good_instance.clone(), &r, &shares).unwrap(); + + // no keys + let mut bad_instance = good_instance.clone(); + bad_instance.public_keys = &[]; + assert!(!chunking_proof.verify(bad_instance)); + + // too many keys + let mut bad_keys = owned_instance.public_keys.clone(); + bad_keys.push(PublicKey( + G1Projective::generator() * Scalar::random(&mut rng), + )); + + let mut bad_instance = good_instance.clone(); + bad_instance.public_keys = &bad_keys; + assert!(!chunking_proof.verify(bad_instance)); + + // too few keys + let mut bad_keys = owned_instance.public_keys.clone(); + bad_keys.truncate(bad_keys.len() - 1); + + let mut bad_instance = good_instance.clone(); + bad_instance.public_keys = &bad_keys; + assert!(!chunking_proof.verify(bad_instance)); + + // no ciphertexts + let mut bad_instance = good_instance.clone(); + bad_instance.ciphertext_chunks = &[]; + assert!(!chunking_proof.verify(bad_instance)); + + // too many ciphertexts + let mut bad_ciphertexts = owned_instance.ciphertext_chunks.clone(); + bad_ciphertexts.push(Default::default()); + + let mut bad_instance = good_instance.clone(); + bad_instance.ciphertext_chunks = &bad_ciphertexts; + assert!(!chunking_proof.verify(bad_instance)); + + // too few ciphertexts + let mut bad_ciphertexts = owned_instance.ciphertext_chunks.clone(); + bad_ciphertexts.truncate(bad_ciphertexts.len() - 1); + + let mut bad_instance = good_instance.clone(); + bad_instance.ciphertext_chunks = &bad_ciphertexts; + assert!(!chunking_proof.verify(bad_instance)); + } + + #[test] + fn should_fail_to_verify_proof_with_wrong_instance() { + let dummy_seed = [1u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + + let (owned_instance, r, shares) = setup(&mut rng); + let instance = Instance { + public_keys: &owned_instance.public_keys, + rr: &owned_instance.randomizers_r, + ciphertext_chunks: &owned_instance.ciphertext_chunks, + }; + + let chunking_proof = + ProofOfChunking::construct(&mut rng, instance.clone(), &r, &shares).unwrap(); + + let (owned_instance2, _, _) = setup(&mut rng); + let bad_instance = Instance { + public_keys: &owned_instance2.public_keys, + rr: &owned_instance2.randomizers_r, + ciphertext_chunks: &owned_instance2.ciphertext_chunks, + }; + + assert!(!chunking_proof.verify(bad_instance)); + } + + #[test] + fn should_fail_to_verify_invalid_proof() { + let dummy_seed = [1u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + + let (owned_instance, r, shares) = setup(&mut rng); + let instance = Instance { + public_keys: &owned_instance.public_keys, + rr: &owned_instance.randomizers_r, + ciphertext_chunks: &owned_instance.ciphertext_chunks, + }; + + let good_proof = + ProofOfChunking::construct(&mut rng, instance.clone(), &r, &shares).unwrap(); + + // essentially mess with every field in some way + + let mut bad_proof = good_proof.clone(); + bad_proof.y0 = G1Projective::generator(); + assert!(!bad_proof.verify(instance.clone())); + + //// + + let mut bad_proof = good_proof.clone(); + bad_proof.bb[0] = G1Projective::generator(); + assert!(!bad_proof.verify(instance.clone())); + + let mut bad_proof = good_proof.clone(); + bad_proof.bb.push(G1Projective::generator()); + assert!(!bad_proof.verify(instance.clone())); + + let mut bad_proof = good_proof.clone(); + bad_proof.bb.truncate(bad_proof.bb.len() - 1); + assert!(!bad_proof.verify(instance.clone())); + + //// + + let mut bad_proof = good_proof.clone(); + bad_proof.cc[0] = G1Projective::generator(); + assert!(!bad_proof.verify(instance.clone())); + + let mut bad_proof = good_proof.clone(); + bad_proof.cc.push(G1Projective::generator()); + assert!(!bad_proof.verify(instance.clone())); + + let mut bad_proof = good_proof.clone(); + bad_proof.cc.truncate(bad_proof.cc.len() - 1); + assert!(!bad_proof.verify(instance.clone())); + + //// + + let mut bad_proof = good_proof.clone(); + bad_proof.dd[0] = G1Projective::generator(); + assert!(!bad_proof.verify(instance.clone())); + + let mut bad_proof = good_proof.clone(); + bad_proof.dd.push(G1Projective::generator()); + assert!(!bad_proof.verify(instance.clone())); + + let mut bad_proof = good_proof.clone(); + bad_proof.dd.truncate(bad_proof.dd.len() - 1); + assert!(!bad_proof.verify(instance.clone())); + + //// + + let mut bad_proof = good_proof.clone(); + bad_proof.yy = G1Projective::generator(); + assert!(!bad_proof.verify(instance.clone())); + + //// + + let mut bad_proof = good_proof.clone(); + bad_proof.responses_r[0] = Scalar::one(); + assert!(!bad_proof.verify(instance.clone())); + + let mut bad_proof = good_proof.clone(); + bad_proof.responses_r.push(Scalar::one()); + assert!(!bad_proof.verify(instance.clone())); + + let mut bad_proof = good_proof.clone(); + bad_proof + .responses_r + .truncate(bad_proof.responses_r.len() - 1); + assert!(!bad_proof.verify(instance.clone())); + + //// + + let mut bad_proof = good_proof.clone(); + bad_proof.responses_chunks[0] = 12345; + assert!(!bad_proof.verify(instance.clone())); + + let mut bad_proof = good_proof.clone(); + bad_proof.responses_chunks.push(12345); + assert!(!bad_proof.verify(instance.clone())); + + let mut bad_proof = good_proof.clone(); + bad_proof + .responses_chunks + .truncate(bad_proof.responses_chunks.len() - 1); + assert!(!bad_proof.verify(instance.clone())); + + //// + + let mut bad_proof = good_proof; + bad_proof.response_beta = Scalar::one(); + assert!(!bad_proof.verify(instance)); + } + + #[test] + fn proof_of_chunking_roundtrip() { + let dummy_seed = [1u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + + let (owned_instance, r, shares) = setup(&mut rng); + let instance = Instance { + public_keys: &owned_instance.public_keys, + rr: &owned_instance.randomizers_r, + ciphertext_chunks: &owned_instance.ciphertext_chunks, + }; + + let good_proof = + ProofOfChunking::construct(&mut rng, instance.clone(), &r, &shares).unwrap(); + + let bytes = good_proof.to_bytes(); + let recovered = ProofOfChunking::try_from_bytes(&bytes).unwrap(); + assert_eq!(good_proof, recovered) + } +} diff --git a/common/crypto/dkg/src/bte/proof_discrete_log.rs b/common/crypto/dkg/src/bte/proof_discrete_log.rs new file mode 100644 index 0000000000..feff6c1603 --- /dev/null +++ b/common/crypto/dkg/src/bte/proof_discrete_log.rs @@ -0,0 +1,94 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::utils::hash_to_scalar; +use bls12_381::{G1Projective, Scalar}; +use ff::Field; +use group::GroupEncoding; +use rand_core::RngCore; +use zeroize::Zeroize; + +// Domain tries to follow guidelines specified by: +// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#section-3.1 +const DISCRETE_LOG_DOMAIN: &[u8] = + b"NYM_COCONUT_NIDKG_V01_CS01_WITH_BLS12381_XMD:SHA-256_SSWU_RO_PROOF_DISCRETE_LOG"; + +#[derive(Debug)] +#[cfg_attr(test, derive(PartialEq))] +pub struct ProofOfDiscreteLog { + pub(crate) rand_commitment: G1Projective, + pub(crate) response: Scalar, +} + +impl ProofOfDiscreteLog { + pub fn construct(mut rng: impl RngCore, public: &G1Projective, witness: &Scalar) -> Self { + let mut rand_x = Scalar::random(&mut rng); + let rand_commitment = G1Projective::generator() * rand_x; + let challenge = Self::compute_challenge(public, &rand_commitment); + + let response = rand_x + challenge * witness; + rand_x.zeroize(); + + ProofOfDiscreteLog { + rand_commitment, + response, + } + } + + // note: we don't have to explicitly check whether points are on correct curves / fields + // as if they weren't, they'd fail to get deserialized + pub fn verify(&self, public: &G1Projective) -> bool { + let challenge = Self::compute_challenge(public, &self.rand_commitment); + + // y^c • a == g1^rand_x + public * challenge + self.rand_commitment == G1Projective::generator() * self.response + } + + pub(crate) fn compute_challenge(public: &G1Projective, rand_commit: &G1Projective) -> Scalar { + let public_bytes = public.to_bytes(); + let rand_commit_bytes = rand_commit.to_bytes(); + + let mut bytes = Vec::with_capacity(96); + bytes.extend_from_slice(public_bytes.as_ref()); + bytes.extend_from_slice(rand_commit_bytes.as_ref()); + + hash_to_scalar(bytes, DISCRETE_LOG_DOMAIN) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rand_core::SeedableRng; + + #[test] + fn should_verify_a_valid_proof() { + let dummy_seed = [1u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + + let witness = Scalar::random(&mut rng); + let public = G1Projective::generator() * witness; + + let proof = ProofOfDiscreteLog::construct(&mut rng, &public, &witness); + + assert!(proof.verify(&public)) + } + + #[test] + fn should_fail_on_invalid_proof() { + let dummy_seed = [1u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + + let witness = Scalar::random(&mut rng); + let public = G1Projective::generator() * witness; + + let other_witness = Scalar::random(&mut rng); + let other_public = G1Projective::generator() * other_witness; + + let proof = ProofOfDiscreteLog::construct(&mut rng, &public, &witness); + let other_proof = ProofOfDiscreteLog::construct(&mut rng, &other_public, &other_witness); + + assert!(!proof.verify(&other_public)); + assert!(!other_proof.verify(&public)); + } +} diff --git a/common/crypto/dkg/src/bte/proof_sharing.rs b/common/crypto/dkg/src/bte/proof_sharing.rs new file mode 100644 index 0000000000..9dc0be4f83 --- /dev/null +++ b/common/crypto/dkg/src/bte/proof_sharing.rs @@ -0,0 +1,615 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::bte::PublicKey; +use crate::error::DkgError; +use crate::interpolation::polynomial::PublicCoefficients; +use crate::utils::{deserialize_g1, deserialize_g2, deserialize_scalar, hash_to_scalar}; +use crate::{NodeIndex, Share}; +use bls12_381::{G1Projective, G2Projective, Scalar}; +use ff::Field; +use group::GroupEncoding; +use rand_core::RngCore; +use std::collections::BTreeMap; + +// Domain tries to follow guidelines specified by: +// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#section-3.1 +const INSTANCE_DOMAIN: &[u8] = + b"NYM_COCONUT_NIDKG_V01_CS01_WITH_BLS12381_XMD:SHA-256_SSWU_RO_PROOF_SECRET_SHARING_INSTANCE"; + +const CHALLENGE_DOMAIN: &[u8] = + b"NYM_COCONUT_NIDKG_V01_CS01_WITH_BLS12381_XMD:SHA-256_SSWU_RO_PROOF_SECRET_SHARING_CHALLENGE"; + +#[cfg_attr(test, derive(Clone))] +pub struct Instance<'a> { + public_keys: &'a BTreeMap, + public_coefficients: &'a PublicCoefficients, + combined_randomizer: &'a G1Projective, + combined_ciphertexts: &'a [G1Projective], +} + +impl<'a> Instance<'a> { + pub fn new( + public_keys: &'a BTreeMap, + public_coefficients: &'a PublicCoefficients, + combined_randomizer: &'a G1Projective, + combined_ciphertexts: &'a [G1Projective], + ) -> Instance<'a> { + Instance { + public_keys, + public_coefficients, + combined_randomizer, + combined_ciphertexts, + } + } + + fn hash_to_scalar(&self) -> Scalar { + let g1s = self.public_keys.len() + 1 + self.combined_ciphertexts.len(); + let g2s = self.public_coefficients.size(); + let mut bytes = Vec::with_capacity(g1s * 48 + g2s * 96); + + for pk in self.public_keys.values() { + bytes.extend_from_slice(pk.0.to_bytes().as_ref()) + } + for coeff in self.public_coefficients.inner() { + bytes.extend_from_slice(coeff.to_bytes().as_ref()) + } + bytes.extend_from_slice(self.combined_randomizer.to_bytes().as_ref()); + + for ciphertext in self.combined_ciphertexts { + bytes.extend_from_slice(ciphertext.to_bytes().as_ref()) + } + + hash_to_scalar(&bytes, INSTANCE_DOMAIN) + } + + fn validate(&self) -> bool { + if self.public_keys.is_empty() || self.public_coefficients.is_empty() { + return false; + } + + if self.public_keys.len() != self.combined_ciphertexts.len() { + return false; + } + + true + } +} + +#[derive(Debug)] +#[cfg_attr(test, derive(Clone, PartialEq))] +pub struct ProofOfSecretSharing { + ff: G1Projective, + aa: G2Projective, + yy: G1Projective, + response_r: Scalar, + response_alpha: Scalar, +} + +impl ProofOfSecretSharing { + pub fn construct( + mut rng: impl RngCore, + instance: Instance, + witness_r: &Scalar, + witnesses_s: &[Share], + ) -> Result { + if !instance.validate() { + return Err(DkgError::MalformedProofOfSharingInstance); + } + + let g1 = G1Projective::generator(); + let g2 = G2Projective::generator(); + + let x = instance.hash_to_scalar(); + + // alpha, rho ← random_scalars + let alpha = Scalar::random(&mut rng); + let rho = Scalar::random(&mut rng); + + // F = g1^rho + let ff = g1 * rho; + // A = g2^alpha + let aa = g2 * alpha; + + // Y = (y_1^{x^1} • ... y_n^{x^n})^rho • g1^alpha + // produce intermediate product (y_1^{x^1} • ... y_n^{x^n}) + let product = + instance + .public_keys + .values() + .rev() + .fold(G1Projective::identity(), |mut acc, pk| { + acc += pk.0; + acc *= x; + acc + }); + let yy = product * rho + g1 * alpha; + + let challenge = Self::compute_challenge(&x, &ff, &aa, &yy); + + // response_r = r • challenge + rho + let response_r = witness_r * challenge + rho; + + // response_alpha = (share_1 • x^1 + ... share_n • x^n) • challenge + alpha + // produce intermediate sum (share_1 • x^1 + ... share_n • x^n) + let sum = witnesses_s + .iter() + .rev() + .fold(Scalar::zero(), |mut acc, witness| { + acc += witness.inner(); + acc *= x; + acc + }); + let response_alpha = sum * challenge + alpha; + + Ok(ProofOfSecretSharing { + ff, + aa, + yy, + response_r, + response_alpha, + }) + } + + pub fn verify(&self, instance: Instance) -> bool { + if !instance.validate() { + return false; + } + + let g1 = G1Projective::generator(); + let g2 = G2Projective::generator(); + + let x = instance.hash_to_scalar(); + let challenge = Self::compute_challenge(&x, &self.ff, &self.aa, &self.yy); + + // check if R^challenge * F == g1^response_r + if instance.combined_randomizer * challenge + self.ff != g1 * self.response_r { + return false; + } + + // check if + // (A_0 ^ (id1^0 • x^1 + ... idn^0 • x^n) • ... A_{t-1} ^ (id1^{t-1} • x^{t-1} + ... idn^{t-1} • x^n))^challenge * A + // == + // g2^response_alpha + let product = instance + .public_coefficients + .inner() + .iter() + .enumerate() + .fold(G2Projective::identity(), |mut acc, (k, coeff)| { + // intermediate (id1^k • x^1 + ... + idn^k • x^n) sum + let sum: Scalar = instance + .public_keys + .keys() + .enumerate() + .map(|(i, node_id)| { + let id_scalar = Scalar::from(*node_id); + id_scalar.pow(&[k as u64, 0, 0, 0]) * x.pow(&[(i + 1) as u64, 0, 0, 0]) + }) + .sum(); + + acc += coeff * sum; + acc + }); + + if product * challenge + self.aa != g2 * self.response_alpha { + return false; + } + + // check if + // (ciphertext_1 ^ (x^1) • ... ciphertext_n ^ (x^n)) ^ challenge • Y + // == + // (pk_1 ^ (x^1) • ... pk_n ^ (x^n)) ^ response_r • g1^response_alpha + + let product_1 = instance.combined_ciphertexts.iter().rev().fold( + G1Projective::identity(), + |mut acc, ciphertext| { + acc += ciphertext; + acc *= x; + acc + }, + ); + + let product_2 = + instance + .public_keys + .values() + .rev() + .fold(G1Projective::identity(), |mut acc, pk| { + acc += pk.0; + acc *= x; + acc + }); + + if product_1 * challenge + self.yy != product_2 * self.response_r + g1 * self.response_alpha + { + return false; + } + + true + } + + pub(crate) fn compute_challenge( + commitment: &Scalar, + blinder_g1: &G1Projective, + blinder_g2: &G2Projective, + blinded_instance: &G1Projective, + ) -> Scalar { + let mut bytes = Vec::with_capacity(224); + + bytes.extend_from_slice(commitment.to_bytes().as_ref()); + bytes.extend_from_slice(blinder_g1.to_bytes().as_ref()); + bytes.extend_from_slice(blinder_g2.to_bytes().as_ref()); + bytes.extend_from_slice(blinded_instance.to_bytes().as_ref()); + + hash_to_scalar(&bytes, CHALLENGE_DOMAIN) + } + + pub(crate) fn to_bytes(&self) -> Vec { + // we have 2 G1 elements, single G2 element and 2 scalars + let mut bytes = Vec::with_capacity(2 * 48 + 96 + 2 * 32); + bytes.extend_from_slice(self.ff.to_bytes().as_ref()); + bytes.extend_from_slice(self.aa.to_bytes().as_ref()); + bytes.extend_from_slice(self.yy.to_bytes().as_ref()); + bytes.extend_from_slice(self.response_r.to_bytes().as_ref()); + bytes.extend_from_slice(self.response_alpha.to_bytes().as_ref()); + bytes + } + + pub(crate) fn try_from_bytes(bytes: &[u8]) -> Result { + if bytes.len() != 2 * 48 + 96 + 2 * 32 { + return Err(DkgError::new_deserialization_failure( + "ProofOfSecretSharing", + "invalid number of bytes provided", + )); + } + + let mut i = 0; + let f = deserialize_g1(&bytes[i..i + 48]).ok_or_else(|| { + DkgError::new_deserialization_failure("ProofOfSecretSharing.f", "invalid curve point") + })?; + i += 48; + + let a = deserialize_g2(&bytes[i..i + 96]).ok_or_else(|| { + DkgError::new_deserialization_failure("ProofOfSecretSharing.a", "invalid curve point") + })?; + i += 96; + + let y = deserialize_g1(&bytes[i..i + 48]).ok_or_else(|| { + DkgError::new_deserialization_failure("ProofOfSecretSharing.y", "invalid curve point") + })?; + i += 48; + + let response_r = deserialize_scalar(&bytes[i..i + 32]).ok_or_else(|| { + DkgError::new_deserialization_failure( + "ProofOfSecretSharing.response_r", + "invalid scalar", + ) + })?; + i += 32; + + let response_alpha = deserialize_scalar(&bytes[i..]).ok_or_else(|| { + DkgError::new_deserialization_failure( + "ProofOfSecretSharing.response_alpha", + "invalid scalar", + ) + })?; + + Ok(ProofOfSecretSharing { + ff: f, + aa: a, + yy: y, + response_r, + response_alpha, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::interpolation::polynomial::Polynomial; + use group::Group; + use rand_core::SeedableRng; + + const NODES: u64 = 50; + const THRESHOLD: u64 = 40; + + fn setup( + mut rng: impl RngCore, + ) -> ( + BTreeMap, + PublicCoefficients, + G1Projective, + Vec, + Scalar, + Vec, + ) { + let g1 = G1Projective::generator(); + + let mut pks = BTreeMap::new(); + let polynomial = Polynomial::new_random(&mut rng, THRESHOLD - 1); + let public_coefficients = polynomial.public_coefficients(); + + let mut shares: Vec = Vec::new(); + let mut node_indices = (0..NODES).map(|_| rng.next_u64()).collect::>(); + node_indices.sort_unstable(); + + for node_index in node_indices { + let share = polynomial.evaluate_at(&Scalar::from(node_index)); + shares.push(share.into()); + pks.insert(node_index, PublicKey(g1 * Scalar::random(&mut rng))); + } + + let r = Scalar::random(&mut rng); + let rr = g1 * r; + + let ciphertexts = pks + .values() + .zip(&shares) + .map(|(pk, share)| pk.0 * r + g1 * share.inner()) + .collect(); + (pks, public_coefficients, rr, ciphertexts, r, shares) + } + + #[test] + fn should_fail_to_create_proof_with_invalid_instance() { + let dummy_seed = [1u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + + let g1 = G1Projective::generator(); + + let mut pks = BTreeMap::new(); + let polynomial = Polynomial::new_random(&mut rng, THRESHOLD - 1); + let public_coefficients = polynomial.public_coefficients(); + + let mut shares: Vec = Vec::new(); + for _ in 0..NODES { + let node_index = rng.next_u64(); + let share = polynomial.evaluate_at(&Scalar::from(node_index)); + shares.push(share.into()); + pks.insert(node_index, PublicKey(g1 * Scalar::random(&mut rng))); + } + + let r = Scalar::random(&mut rng); + let rr = g1 * r; + + let mut shares = Vec::new(); + for node_id in 1..NODES + 1 { + let share = polynomial.evaluate_at(&Scalar::from(node_id)); + shares.push(share); + } + + let ciphertexts = pks + .values() + .zip(&shares) + .map(|(pk, share)| pk.0 * r + g1 * share) + .collect::>(); + + // no public keys + let bad_instance1 = Instance { + public_keys: &BTreeMap::new(), + public_coefficients: &public_coefficients, + combined_randomizer: &rr, + combined_ciphertexts: &ciphertexts, + }; + assert!(!bad_instance1.validate()); + + // no public coefficients + let bad_instance2 = Instance { + public_keys: &pks, + public_coefficients: &PublicCoefficients { + coefficients: Vec::new(), + }, + combined_randomizer: &rr, + combined_ciphertexts: &ciphertexts, + }; + assert!(!bad_instance2.validate()); + + // no ciphertexts + let bad_instance3 = Instance { + public_keys: &pks, + public_coefficients: &public_coefficients, + combined_randomizer: &rr, + combined_ciphertexts: &[], + }; + assert!(!bad_instance3.validate()); + + // public_keys.len() != combined_ciphertexts.len() + let bad_ciphertexts = ciphertexts.iter().skip(1).cloned().collect::>(); + + let bad_instance4 = Instance { + public_keys: &pks, + public_coefficients: &public_coefficients, + combined_randomizer: &rr, + combined_ciphertexts: &bad_ciphertexts, + }; + assert!(!bad_instance4.validate()); + + // changed index of one of the keys + let mut bad_pks = pks.clone(); + let first_id = bad_pks.keys().copied().take(1).collect::>(); + let first_val = bad_pks.remove(&first_id[0]).unwrap(); + bad_pks.insert(rng.next_u64(), first_val); + + let bad_instance5 = Instance { + public_keys: &bad_pks, + public_coefficients: &public_coefficients, + combined_randomizer: &rr, + combined_ciphertexts: &bad_ciphertexts, + }; + assert!(!bad_instance5.validate()); + + let good_instance = Instance { + public_keys: &pks, + public_coefficients: &public_coefficients, + combined_randomizer: &rr, + combined_ciphertexts: &ciphertexts, + }; + assert!(good_instance.validate()) + } + + #[test] + fn should_verify_a_valid_proof() { + let dummy_seed = [1u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + + let (public_keys, public_coefficients, rr, ciphertexts, r, shares) = setup(&mut rng); + + let instance = Instance { + public_keys: &public_keys, + public_coefficients: &public_coefficients, + combined_randomizer: &rr, + combined_ciphertexts: &ciphertexts, + }; + + let sharing_proof = + ProofOfSecretSharing::construct(&mut rng, instance.clone(), &r, &shares).unwrap(); + + assert!(sharing_proof.verify(instance)) + } + + #[test] + fn should_fail_to_verify_proof_with_invalid_instance() { + let dummy_seed = [1u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + + let (public_keys, public_coefficients, rr, ciphertexts, r, shares) = setup(&mut rng); + + let instance = Instance { + public_keys: &public_keys, + public_coefficients: &public_coefficients, + combined_randomizer: &rr, + combined_ciphertexts: &ciphertexts, + }; + + let sharing_proof = + ProofOfSecretSharing::construct(&mut rng, instance.clone(), &r, &shares).unwrap(); + + // no public keys + let bad_instance1 = Instance { + public_keys: &BTreeMap::new(), + public_coefficients: &public_coefficients, + combined_randomizer: &rr, + combined_ciphertexts: &ciphertexts, + }; + assert!(!sharing_proof.verify(bad_instance1)); + + // no public coefficients + let bad_instance2 = Instance { + public_keys: &public_keys, + public_coefficients: &PublicCoefficients { + coefficients: Vec::new(), + }, + combined_randomizer: &rr, + combined_ciphertexts: &ciphertexts, + }; + assert!(!sharing_proof.verify(bad_instance2)); + + // no ciphertexts + let bad_instance3 = Instance { + public_keys: &public_keys, + public_coefficients: &public_coefficients, + combined_randomizer: &rr, + combined_ciphertexts: &[], + }; + assert!(!sharing_proof.verify(bad_instance3)); + + // public_keys.len() != combined_ciphertexts.len() + let bad_ciphertexts = ciphertexts.iter().skip(1).cloned().collect::>(); + + let bad_instance4 = Instance { + public_keys: &public_keys, + public_coefficients: &public_coefficients, + combined_randomizer: &rr, + combined_ciphertexts: &bad_ciphertexts, + }; + assert!(!sharing_proof.verify(bad_instance4)); + } + + #[test] + fn should_fail_to_verify_proof_with_wrong_instance() { + let dummy_seed = [1u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + + let (public_keys, public_coefficients, rr, ciphertexts, r, shares) = setup(&mut rng); + + let instance = Instance { + public_keys: &public_keys, + public_coefficients: &public_coefficients, + combined_randomizer: &rr, + combined_ciphertexts: &ciphertexts, + }; + + let sharing_proof = + ProofOfSecretSharing::construct(&mut rng, instance.clone(), &r, &shares).unwrap(); + + let (public_keys, public_coefficients, rr, ciphertexts, _, _) = setup(&mut rng); + let bad_instance = Instance { + public_keys: &public_keys, + public_coefficients: &public_coefficients, + combined_randomizer: &rr, + combined_ciphertexts: &ciphertexts, + }; + + assert!(!sharing_proof.verify(bad_instance)); + } + + #[test] + fn should_fail_to_verify_invalid_proof() { + let dummy_seed = [1u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + + let (public_keys, public_coefficients, rr, ciphertexts, r, shares) = setup(&mut rng); + + let instance = Instance { + public_keys: &public_keys, + public_coefficients: &public_coefficients, + combined_randomizer: &rr, + combined_ciphertexts: &ciphertexts, + }; + + let good_proof = + ProofOfSecretSharing::construct(&mut rng, instance.clone(), &r, &shares).unwrap(); + + let mut bad_proof = good_proof.clone(); + bad_proof.ff = G1Projective::generator(); + assert!(!bad_proof.verify(instance.clone())); + + let mut bad_proof = good_proof.clone(); + bad_proof.aa = G2Projective::generator(); + assert!(!bad_proof.verify(instance.clone())); + + let mut bad_proof = good_proof.clone(); + bad_proof.yy = G1Projective::generator(); + assert!(!bad_proof.verify(instance.clone())); + + let mut bad_proof = good_proof.clone(); + bad_proof.response_r = Scalar::from(42); + assert!(!bad_proof.verify(instance.clone())); + + let mut bad_proof = good_proof; + bad_proof.response_alpha = Scalar::from(42); + assert!(!bad_proof.verify(instance)); + } + + #[test] + fn proof_of_secret_sharing_roundtrip() { + let dummy_seed = [1u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + + let proof_fixture = ProofOfSecretSharing { + ff: G1Projective::random(&mut rng), + aa: G2Projective::random(&mut rng), + yy: G1Projective::random(&mut rng), + response_r: Scalar::random(&mut rng), + response_alpha: Scalar::random(&mut rng), + }; + + let bytes = proof_fixture.to_bytes(); + let recovered = ProofOfSecretSharing::try_from_bytes(&bytes).unwrap(); + assert_eq!(proof_fixture, recovered); + + assert!(ProofOfSecretSharing::try_from_bytes(&bytes[1..]).is_err()) + } +} diff --git a/common/crypto/dkg/src/dealing.rs b/common/crypto/dkg/src/dealing.rs new file mode 100644 index 0000000000..8b214b58c7 --- /dev/null +++ b/common/crypto/dkg/src/dealing.rs @@ -0,0 +1,500 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::bte::proof_chunking::ProofOfChunking; +use crate::bte::proof_sharing::ProofOfSecretSharing; +use crate::bte::{ + encrypt_shares, proof_chunking, proof_sharing, Ciphertexts, Epoch, Params, PublicKey, +}; +use crate::error::DkgError; +use crate::interpolation::polynomial::{Polynomial, PublicCoefficients}; +use crate::interpolation::{ + perform_lagrangian_interpolation_at_origin, perform_lagrangian_interpolation_at_x, +}; +use crate::{NodeIndex, Share, Threshold}; +use bls12_381::{G2Projective, Scalar}; +use rand_core::RngCore; +use std::collections::BTreeMap; +use zeroize::Zeroize; + +#[derive(Debug)] +#[cfg_attr(test, derive(PartialEq))] +pub struct Dealing { + pub public_coefficients: PublicCoefficients, + pub ciphertexts: Ciphertexts, + pub proof_of_chunking: ProofOfChunking, + pub proof_of_sharing: ProofOfSecretSharing, +} + +impl Dealing { + // I'm not a big fan of this function signature, but I'm not clear on how to improve it while + // allowing the dealer to skip decryption of its own share if it was also one of the receivers + pub fn create( + mut rng: impl RngCore, + params: &Params, + dealer_index: NodeIndex, + threshold: Threshold, + epoch: Epoch, + // BTreeMap ensures the keys are sorted by their indices + receivers: &BTreeMap, + prior_resharing_secret: Option, + ) -> (Self, Option) { + assert!(threshold > 0); + + let mut polynomial = Polynomial::new_random(&mut rng, threshold - 1); + if let Some(prior_secret) = prior_resharing_secret { + polynomial.set_constant_coefficient(prior_secret) + } + + let mut shares = receivers + .keys() + .map(|&node_index| polynomial.evaluate_at(&Scalar::from(node_index)).into()) + .collect::>(); + + let remote_share_key_pairs = shares + .iter() + .zip(receivers.values()) + .map(|(share, key)| (share, key)) + .collect::>(); + let ordered_public_keys = receivers.values().copied().collect::>(); + + let (ciphertexts, hazmat) = + encrypt_shares(&remote_share_key_pairs, epoch, params, &mut rng); + + // create proofs of knowledge + let chunking_instance = proof_chunking::Instance::new(&ordered_public_keys, &ciphertexts); + let proof_of_chunking = + ProofOfChunking::construct(&mut rng, chunking_instance, hazmat.r(), &shares) + .expect("failed to construct proof of chunking"); + + let combined_ciphertexts = ciphertexts.combine_ciphertexts(); + let mut combined_r = hazmat.combine_rs(); + let combined_rr = ciphertexts.combine_rs(); + + let public_coefficients = polynomial.public_coefficients(); + let sharing_instance = proof_sharing::Instance::new( + receivers, + &public_coefficients, + &combined_rr, + &combined_ciphertexts, + ); + let proof_of_sharing = + ProofOfSecretSharing::construct(&mut rng, sharing_instance, &combined_r, &shares) + .expect("failed to construct proof of secret sharing"); + + combined_r.zeroize(); + + let dealing = Dealing { + public_coefficients, + ciphertexts, + proof_of_chunking, + proof_of_sharing, + }; + + let dealers_key_index = receivers + .keys() + .position(|node_index| node_index == &dealer_index); + if let Some(dealer_key_index) = dealers_key_index { + let dealers_share = shares.remove(dealer_key_index); + shares.zeroize(); + (dealing, Some(dealers_share)) + } else { + (dealing, None) + } + } + + // rather than returning a bool for whether the dealing is valid or not, a Result is returned + // instead so that we would have more information regarding a possible failure cause + pub fn verify( + &self, + params: &Params, + epoch: Epoch, + threshold: Threshold, + receivers: &BTreeMap, + prior_resharing_public: Option, + ) -> Result<(), DkgError> { + if threshold == 0 || threshold as usize > receivers.len() { + return Err(DkgError::InvalidThreshold { + actual: threshold as usize, + participating: receivers.len(), + }); + } + + if self.ciphertexts.ciphertext_chunks.len() != receivers.len() { + return Err(DkgError::WrongCiphertextSize { + actual: self.ciphertexts.ciphertext_chunks.len(), + expected: receivers.len(), + }); + } + + if self.public_coefficients.size() != threshold as usize { + return Err(DkgError::WrongPublicCoefficientsSize { + actual: self.public_coefficients.size(), + expected: threshold as usize, + }); + } + + if !self.ciphertexts.verify_integrity(params, epoch) { + return Err(DkgError::FailedCiphertextIntegrityCheck); + } + + // TODO: perhaps change the underlying arguments in proofs of knowledge to avoid this allocation? + let sorted_receivers = receivers.values().copied().collect::>(); + + let chunking_instance = proof_chunking::Instance::new(&sorted_receivers, &self.ciphertexts); + if !self.proof_of_chunking.verify(chunking_instance) { + return Err(DkgError::InvalidProofOfChunking); + } + + let combined_randomizer = &self.ciphertexts.combine_rs(); + let combined_ciphertexts = &self.ciphertexts.combine_ciphertexts(); + + let sharing_instance = proof_sharing::Instance::new( + receivers, + &self.public_coefficients, + combined_randomizer, + combined_ciphertexts, + ); + + if !self.proof_of_sharing.verify(sharing_instance) { + return Err(DkgError::InvalidProofOfSharing); + } + + if let Some(prior_public) = prior_resharing_public { + let dealt_public = &self.public_coefficients[0]; + if dealt_public != &prior_public { + return Err(DkgError::InvalidResharing); + } + } + + Ok(()) + } + + // coeff_len || coeff || cc_len || cc || pi_c_len || pi_c || pi_s_len || pi_s + pub fn to_bytes(&self) -> Vec { + let mut bytes = Vec::new(); + + let mut coefficients_bytes = self.public_coefficients.to_bytes(); + bytes.extend_from_slice(&(coefficients_bytes.len() as u32).to_be_bytes()); + bytes.append(&mut coefficients_bytes); + + let mut ciphertexts_bytes = self.ciphertexts.to_bytes(); + bytes.extend_from_slice(&(ciphertexts_bytes.len() as u32).to_be_bytes()); + bytes.append(&mut ciphertexts_bytes); + + let mut proof_sharing_bytes = self.proof_of_sharing.to_bytes(); + bytes.extend_from_slice(&(proof_sharing_bytes.len() as u32).to_be_bytes()); + bytes.append(&mut proof_sharing_bytes); + + let mut proof_chunking_bytes = self.proof_of_chunking.to_bytes(); + bytes.extend_from_slice(&(proof_chunking_bytes.len() as u32).to_be_bytes()); + bytes.append(&mut proof_chunking_bytes); + + bytes + } + + pub fn try_from_bytes(bytes: &[u8]) -> Result { + // can we read the length of serialized public coefficients? + if bytes.len() < 4 { + return Err(DkgError::new_deserialization_failure( + "Dealing", + "insufficient number of bytes provided", + )); + } + + let mut i = 0; + let coefficients_bytes_len = + u32::from_be_bytes((&bytes[i..i + 4]).try_into().unwrap()) as usize; + i += 4; + let public_coefficients = + PublicCoefficients::try_from_bytes(&bytes[i..i + coefficients_bytes_len])?; + i += coefficients_bytes_len; + + let ciphertexts_bytes_len = + u32::from_be_bytes((&bytes[i..i + 4]).try_into().unwrap()) as usize; + i += 4; + let ciphertexts = Ciphertexts::try_from_bytes(&bytes[i..i + ciphertexts_bytes_len])?; + i += ciphertexts_bytes_len; + + let proof_of_sharing_bytes_len = + u32::from_be_bytes((&bytes[i..i + 4]).try_into().unwrap()) as usize; + i += 4; + let proof_of_sharing = + ProofOfSecretSharing::try_from_bytes(&bytes[i..i + proof_of_sharing_bytes_len])?; + i += proof_of_sharing_bytes_len; + + let proof_of_chunking_bytes_len = + u32::from_be_bytes((&bytes[i..i + 4]).try_into().unwrap()) as usize; + i += 4; + + if bytes[i..].len() != proof_of_chunking_bytes_len { + return Err(DkgError::new_deserialization_failure( + "Dealing", + "invalid number of bytes provided", + )); + } + + let proof_of_chunking = ProofOfChunking::try_from_bytes(&bytes[i..])?; + + Ok(Dealing { + public_coefficients, + ciphertexts, + proof_of_chunking, + proof_of_sharing, + }) + } +} + +// this assumes all dealings have been verified +pub fn try_recover_verification_keys( + dealings: &[Dealing], + threshold: Threshold, + receivers: &BTreeMap, +) -> Result<(G2Projective, Vec), DkgError> { + if dealings.is_empty() { + return Err(DkgError::NoDealingsAvailable); + } + + let threshold_usize = threshold as usize; + + if !dealings + .iter() + .all(|dealing| dealing.public_coefficients.size() == threshold_usize) + { + return Err(DkgError::MismatchedDealings); + } + + // currently we expect every dealer to also be a receiver. This restriction might be relaxed in the future + if dealings.len() != receivers.len() { + return Err(DkgError::MismatchedDealings); + } + + let indices = receivers.keys().collect::>(); + + // Compute A0, ..., A_{t-1} + let mut interpolated_coefficients = Vec::with_capacity(threshold_usize); + for k in 0..threshold_usize { + let mut samples = Vec::with_capacity(indices.len()); + for (j, dealing) in dealings.iter().enumerate() { + samples.push(( + Scalar::from(*indices[j]), + *dealing.public_coefficients.nth(k), + )) + } + let interpolated = perform_lagrangian_interpolation_at_origin(&samples)?; + interpolated_coefficients.push(interpolated); + } + + let master_verification_key = interpolated_coefficients[0]; + + let interpolated_coefficients = PublicCoefficients { + coefficients: interpolated_coefficients, + }; + + // shvk_j = A0^{j^0} * A1^{j^1} * ... * A_{t-1}^{j^{t-1}} + let verification_key_shares = receivers + .keys() + .map(|index| interpolated_coefficients.evaluate_at(&Scalar::from(*index))) + .collect(); + + Ok((master_verification_key, verification_key_shares)) +} + +pub fn verify_verification_keys( + master_key: &G2Projective, + shares: &[G2Projective], + receivers: &BTreeMap, + threshold: Threshold, +) -> Result<(), DkgError> { + if shares.len() != receivers.len() { + return Err(DkgError::NotEnoughReceiversProvided); + } + + if threshold as usize > receivers.len() { + return Err(DkgError::InvalidThreshold { + actual: threshold as usize, + participating: receivers.len(), + }); + } + + let indices = receivers.keys().copied().collect::>(); + + let indices_with_origin = std::iter::once(&0) + .chain(receivers.keys()) + .collect::>(); + let all_shares = std::iter::once(master_key) + .chain(shares.iter()) + .collect::>(); + + for (i, share) in shares.iter().enumerate() { + let samples = indices_with_origin + .iter() + .zip(all_shares.iter()) + .map(|(&node_index, &share)| (Scalar::from(*node_index), *share)) + .take(threshold as usize) + .collect::>(); + let interpolated = + perform_lagrangian_interpolation_at_x(&Scalar::from(indices[i]), &samples)?; + if share != &interpolated { + return Err(DkgError::MismatchedVerificationKey); + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::bte::{decrypt_share, keygen, setup}; + use crate::combine_shares; + use rand_core::SeedableRng; + + #[test] + fn recovering_partial_verification_keys() { + // START OF SETUP + let dummy_seed = [42u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + let params = setup(); + + let threshold = 2; + let node_indices = vec![1, 4, 7]; + + let mut receivers = BTreeMap::new(); + let mut full_keys = Vec::new(); + for index in &node_indices { + let (dk, pk) = keygen(¶ms, &mut rng); + receivers.insert(*index, *pk.public_key()); + full_keys.push((dk, pk)) + } + + // start off in a defined epoch (i.e. not root); + let epoch = Epoch::new(2); + + let dealings = node_indices + .iter() + .map(|&dealer_index| { + Dealing::create( + &mut rng, + ¶ms, + dealer_index, + threshold, + epoch, + &receivers, + None, + ) + .0 + }) + .collect::>(); + + let mut derived_secrets = Vec::new(); + for (i, (ref mut dk, _)) in full_keys.iter_mut().enumerate() { + dk.try_update_to(epoch, ¶ms, &mut rng).unwrap(); + + let shares = dealings + .iter() + .map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, epoch, None).unwrap()) + .collect(); + derived_secrets.push( + combine_shares(shares, &receivers.keys().copied().collect::>()).unwrap(), + ) + } + + let master_secret = perform_lagrangian_interpolation_at_origin(&[ + (Scalar::from(node_indices[2]), derived_secrets[2]), + (Scalar::from(node_indices[1]), derived_secrets[1]), + ]) + .unwrap(); + + // END OF SETUP + let (recovered_master, recovered_partials) = + try_recover_verification_keys(&dealings, threshold, &receivers).unwrap(); + + let g2 = G2Projective::generator(); + assert_eq!(g2 * master_secret, recovered_master); + + assert_eq!(g2 * derived_secrets[0], recovered_partials[0]); + assert_eq!(g2 * derived_secrets[1], recovered_partials[1]); + assert_eq!(g2 * derived_secrets[2], recovered_partials[2]); + } + + #[test] + fn verifying_partial_verification_keys() { + let dummy_seed = [42u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + let params = setup(); + + let threshold = 2; + let node_indices = vec![1, 4, 7]; + + let mut receivers = BTreeMap::new(); + let mut full_keys = Vec::new(); + for index in &node_indices { + let (dk, pk) = keygen(¶ms, &mut rng); + receivers.insert(*index, *pk.public_key()); + full_keys.push((dk, pk)) + } + + // start off in a defined epoch (i.e. not root); + let epoch = Epoch::new(2); + + let dealings = node_indices + .iter() + .map(|&dealer_index| { + Dealing::create( + &mut rng, + ¶ms, + dealer_index, + threshold, + epoch, + &receivers, + None, + ) + .0 + }) + .collect::>(); + + let (recovered_master, recovered_partials) = + try_recover_verification_keys(&dealings, threshold, &receivers).unwrap(); + + assert!(verify_verification_keys( + &recovered_master, + &recovered_partials, + &receivers, + threshold + ) + .is_ok()) + } + + #[test] + fn dealing_roundtrip() { + let dummy_seed = [1u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + let params = setup(); + + let parties = 5; + let threshold = ((parties as f32 * 2.) / 3. + 1.) as Threshold; + let node_indices = (1..=parties).collect::>(); + let epoch = Epoch::new(2); + + let mut receivers = BTreeMap::new(); + for index in &node_indices { + let (_, pk) = keygen(¶ms, &mut rng); + receivers.insert(*index, *pk.public_key()); + } + + let (dealing, _) = Dealing::create( + &mut rng, + ¶ms, + node_indices[0], + threshold, + epoch, + &receivers, + None, + ); + + let bytes = dealing.to_bytes(); + let recovered = Dealing::try_from_bytes(&bytes).unwrap(); + assert_eq!(dealing, recovered); + } +} diff --git a/common/crypto/dkg/src/error.rs b/common/crypto/dkg/src/error.rs new file mode 100644 index 0000000000..372a00936a --- /dev/null +++ b/common/crypto/dkg/src/error.rs @@ -0,0 +1,114 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum DkgError { + #[error("Provided set of values contained duplicate coordinate")] + DuplicateCoordinate, + + #[error("The public key is malformed")] + MalformedPublicKey, + + #[error("The decryption key is malformed")] + MalformedDecryptionKey, + + #[error("Could not solve the discrete log")] + UnsolvableDiscreteLog, + + #[error("Received share is malformed")] + MalformedShare, + + #[error("The share encrypted under index {0} doesn't exist")] + UnavailableCiphertext(usize), + + #[error("The provided lookup table is mismatched")] + MismatchedLookupTable, + + #[error("Failed to verify proof of discrete logarithm")] + InvalidProofOfDiscreteLog, + + #[error("Tried to construct proof of sharing with an invalid instance")] + MalformedProofOfSharingInstance, + + #[error("Tried to construct proof of chunking with an invalid instance")] + MalformedProofOfChunkingInstance, + + #[error("Aborted construction of proof of chunking - could not complete it within specified number of attempts")] + AbortedProofOfChunking, + + #[error("Tried to update the decryption key to an epoch in the past")] + TargetEpochUpdateInThePast, + + #[error("Provided epoch is malformed")] + MalformedEpoch, + + #[error("Provided node is not a valid parent")] + NotAValidParent, + + #[error("Provided decryption key has expired")] + ExpiredKey, + + #[error("Provided threshold value ({actual}) is either 0 or larger than the total number of the participating parties ({participating})")] + InvalidThreshold { actual: usize, participating: usize }, + + #[error( + "Provided ciphertext has been generated for a different number of participating parties (expected: {expected}, actual: {actual})" + )] + WrongCiphertextSize { actual: usize, expected: usize }, + + #[error( + "Provided public coefficients have been generated for a different number of participating parties (expected: {expected}, actual: {actual})" + )] + WrongPublicCoefficientsSize { actual: usize, expected: usize }, + + #[error("The provided ciphertexts failed integrity check")] + FailedCiphertextIntegrityCheck, + + #[error("The provided proof of secret sharing was invalid")] + InvalidProofOfSharing, + + #[error("The provided proof of chunking was invalid")] + InvalidProofOfChunking, + + #[error("Failed to deserialize {name} - {reason}")] + DeserializationFailure { name: String, reason: String }, + + #[error("No dealings were provided")] + NoDealingsAvailable, + + #[error("Provided dealings were created under different parameters")] + MismatchedDealings, + + #[error( + "Not enough dealings are available. We have {available} while require at least {required}" + )] + NotEnoughDealingsAvailable { available: usize, required: usize }, + + #[error("Received different number of x and y coordinates for lagrangian interpolation (xs: {x}, ys: {y})")] + MismatchedLagrangianSamplesLengths { x: usize, y: usize }, + + #[error("Derived partial verification key is mismatched")] + MismatchedVerificationKey, + + #[error("Insufficient number of receivers was provided")] + NotEnoughReceiversProvided, + + #[error( + "The reshared dealing has different public constant coefficient than its prior variant" + )] + InvalidResharing, +} + +impl DkgError { + pub fn new_deserialization_failure, T: Into>( + name: S, + reason: T, + ) -> DkgError { + DkgError::DeserializationFailure { + name: name.into(), + reason: reason.into(), + } + } +} diff --git a/common/crypto/dkg/src/interpolation/mod.rs b/common/crypto/dkg/src/interpolation/mod.rs new file mode 100644 index 0000000000..2862d84f8a --- /dev/null +++ b/common/crypto/dkg/src/interpolation/mod.rs @@ -0,0 +1,157 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::DkgError; +use bls12_381::Scalar; +use core::iter::Sum; +use core::ops::Mul; +use std::collections::HashSet; + +pub mod polynomial; + +fn contains_duplicates(vals: &[Scalar]) -> bool { + let mut set = HashSet::new(); + + for x in vals { + if !set.insert(x.to_bytes()) { + return true; + } + } + + false +} + +#[inline] +fn generate_lagrangian_coefficients_at_x( + x: &Scalar, + points: &[Scalar], +) -> Result, DkgError> { + let num_points = points.len(); + if num_points == 0 { + return Ok(Vec::new()); + } else if num_points == 1 { + return Ok(vec![Scalar::one()]); + } + + if contains_duplicates(points) { + return Err(DkgError::DuplicateCoordinate); + } + + let mut res = Vec::with_capacity(points.len()); + + for (i, xi) in points.iter().enumerate() { + let mut numerator = Scalar::one(); + let mut denominator = Scalar::one(); + + for (j, xj) in points.iter().enumerate() { + if j != i { + // numerator = (x - xs[0]) * ... * (x - xs[j]), j != i + numerator *= x - xj; + + // denominator = (xs[i] - x[0]) * ... * (xs[i] - x[j]), j != i + denominator *= xi - xj; + } + } + + // 1 / denominator + let inv: Scalar = + Option::from(denominator.invert()).ok_or(DkgError::DuplicateCoordinate)?; + + // numerator / denominator + res.push(numerator * inv) + } + + Ok(res) +} + +/// Performs a Lagrange interpolation at specified x for a polynomial defined by set of coordinates +/// (x, f(x)), where x is a `Scalar` and f(x) is a generic type that can be obtained by evaluating `f` at `x`. +/// It can be used for Scalars, G1 and G2 points. +pub fn perform_lagrangian_interpolation_at_x( + x: &Scalar, + points: &[(Scalar, T)], +) -> Result +where + T: Sum, + for<'a> &'a T: Mul, +{ + let xs = points.iter().map(|p| p.0).collect::>(); + let coefficients = generate_lagrangian_coefficients_at_x(x, &xs)?; + + Ok(coefficients + .into_iter() + .zip(points.iter().map(|p| &p.1)) + .map(|(coeff, y)| y * coeff) + .sum()) +} + +/// Performs a Lagrange interpolation at the origin for a polynomial defined by set of coordinates +/// (x, f(x)), where x is a `Scalar` and f(x) is a generic type that can be obtained by evaluating `f` at `x`. +/// It can be used for Scalars, G1 and G2 points. +pub fn perform_lagrangian_interpolation_at_origin(points: &[(Scalar, T)]) -> Result +where + T: Sum, + for<'a> &'a T: Mul, +{ + perform_lagrangian_interpolation_at_x(&Scalar::zero(), points) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn performing_lagrangian_scalar_interpolation_at_origin() { + // x^2 + 3 + // x, f(x): + // 1, 4, + // 2, 7, + // 3, 12, + let points = vec![ + (Scalar::from(1), Scalar::from(4)), + (Scalar::from(2), Scalar::from(7)), + (Scalar::from(3), Scalar::from(12)), + ]; + assert_eq!( + Scalar::from(3), + perform_lagrangian_interpolation_at_origin(&points).unwrap() + ); + + // x^3 + 3x^2 - 5x + 11 + // x, f(x): + // 1, 10 + // 2, 21 + // 3, 50 + // 4, 103 + let points = vec![ + (Scalar::from(1), Scalar::from(10)), + (Scalar::from(2), Scalar::from(21)), + (Scalar::from(3), Scalar::from(50)), + (Scalar::from(4), Scalar::from(103)), + ]; + assert_eq!( + Scalar::from(11), + perform_lagrangian_interpolation_at_origin(&points).unwrap() + ); + + // more points than it is required + // x^2 + x + 10 + // x, f(x) + // 1, 12 + // 2, 16 + // 3, 22 + // 4, 30 + // 5, 40 + let points = vec![ + (Scalar::from(1), Scalar::from(12)), + (Scalar::from(2), Scalar::from(16)), + (Scalar::from(3), Scalar::from(22)), + (Scalar::from(4), Scalar::from(30)), + (Scalar::from(5), Scalar::from(40)), + ]; + assert_eq!( + Scalar::from(10), + perform_lagrangian_interpolation_at_origin(&points).unwrap() + ); + } +} diff --git a/common/crypto/dkg/src/interpolation/polynomial.rs b/common/crypto/dkg/src/interpolation/polynomial.rs new file mode 100644 index 0000000000..f327535d0c --- /dev/null +++ b/common/crypto/dkg/src/interpolation/polynomial.rs @@ -0,0 +1,395 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::DkgError; +use crate::utils::deserialize_g2; +use bls12_381::{G2Projective, Scalar}; +use ff::Field; +use group::GroupEncoding; +use rand_core::RngCore; +use std::ops::{Add, Index, IndexMut}; +use zeroize::Zeroize; + +#[derive(Clone, Debug, PartialEq)] +pub struct PublicCoefficients { + pub(crate) coefficients: Vec, +} + +impl PublicCoefficients { + pub(crate) fn size(&self) -> usize { + self.coefficients.len() + } + + pub(crate) fn nth(&self, n: usize) -> &G2Projective { + &self.coefficients[n] + } + + pub(crate) fn is_empty(&self) -> bool { + self.coefficients.is_empty() + } + + pub(crate) fn inner(&self) -> &[G2Projective] { + &self.coefficients + } + + pub(crate) fn evaluate_at(&self, x: &Scalar) -> G2Projective { + if self.coefficients.is_empty() { + G2Projective::identity() + // if x is zero then we can ignore most of the expensive computation and + // just return the last term of the polynomial + } else if x.is_zero().into() { + // we checked that coefficients are not empty so unwrap here is fine + *self.coefficients.first().unwrap() + } else { + self.coefficients + .iter() + .enumerate() + // coefficient[n] * x ^ n + .map(|(i, coefficient)| coefficient * x.pow(&[i as u64, 0, 0, 0])) + .sum() + } + } + + pub(crate) fn to_bytes(&self) -> Vec { + let coeffs = self.coefficients.len(); + let mut bytes = Vec::with_capacity(4 + 96 * coeffs); + bytes.extend_from_slice(&((coeffs as u32).to_be_bytes())); + for coeff in &self.coefficients { + bytes.extend_from_slice(coeff.to_bytes().as_ref()) + } + + bytes + } + + pub(crate) fn try_from_bytes(b: &[u8]) -> Result { + if b.len() < 4 { + return Err(DkgError::new_deserialization_failure( + "PublicCoefficients", + "insufficient number of bytes provided", + )); + } + + let coeffs = u32::from_be_bytes([b[0], b[1], b[2], b[3]]) as usize; + let mut coefficients = Vec::with_capacity(coeffs); + + if b.len() != 4 + coeffs * 96 { + return Err(DkgError::new_deserialization_failure( + "PublicCoefficients", + "insufficient number of bytes provided", + )); + } + + let mut i = 4; + for _ in 0..coeffs { + let coefficient = deserialize_g2(&b[i..i + 96]).ok_or_else(|| { + DkgError::new_deserialization_failure( + "PublicCoefficients.coefficient", + "invalid curve point", + ) + })?; + + coefficients.push(coefficient); + i += 96; + } + + Ok(PublicCoefficients { coefficients }) + } +} + +impl Index for PublicCoefficients { + type Output = G2Projective; + + fn index(&self, index: usize) -> &Self::Output { + self.coefficients.index(index) + } +} + +impl IndexMut for PublicCoefficients { + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + self.coefficients.index_mut(index) + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Zeroize)] +#[zeroize(drop)] +pub struct Polynomial { + coefficients: Vec, +} + +impl Polynomial { + // for polynomial of degree n, we generate n+1 values + // (for example for degree 1, like y = x + 2, we need [2,1]) + /// Creates new pseudorandom polynomial of specified degree. + pub fn new_random(mut rng: impl RngCore, degree: u64) -> Self { + Polynomial { + coefficients: (0..=degree).map(|_| Scalar::random(&mut rng)).collect(), + } + } + + /// Creates new polynomial with provided coefficients. + pub fn new(coefficients: Vec) -> Self { + Polynomial { coefficients } + } + + pub fn set_constant_coefficient(&mut self, value: Scalar) { + if self.coefficients.is_empty() { + self.coefficients = vec![value] + } else { + self.coefficients[0] = value + } + } + + /// Creates a zero-polynomial, i.e. p(x) = 0 + pub const fn zero() -> Self { + Polynomial { + coefficients: Vec::new(), + } + } + + /// Returns public coefficients associated with this polynomial. + pub fn public_coefficients(&self) -> PublicCoefficients { + let g2 = G2Projective::generator(); + let coefficients = self.coefficients.iter().map(|a_i| g2 * a_i).collect(); + + PublicCoefficients { coefficients } + } + + /// Evaluates the polynomial at point x. + pub fn evaluate_at(&self, x: &Scalar) -> Scalar { + if self.coefficients.is_empty() { + Scalar::zero() + // if x is zero then we can ignore most of the expensive computation and + // just return the last term of the polynomial + } else if x.is_zero().into() { + // we checked that coefficients are not empty so unwrap here is fine + *self.coefficients.first().unwrap() + } else { + self.coefficients + .iter() + .enumerate() + // coefficient[n] * x ^ n + .map(|(i, coefficient)| coefficient * x.pow(&[i as u64, 0, 0, 0])) + .sum() + } + } +} + +impl Index for Polynomial { + type Output = Scalar; + + fn index(&self, index: usize) -> &Self::Output { + self.coefficients.index(index) + } +} + +impl IndexMut for Polynomial { + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + self.coefficients.index_mut(index) + } +} + +impl<'b> Add<&'b Polynomial> for Polynomial { + type Output = Polynomial; + + fn add(self, rhs: &'b Polynomial) -> Polynomial { + &self + rhs + } +} + +impl<'a> Add for &'a Polynomial { + type Output = Polynomial; + + fn add(self, rhs: Polynomial) -> Polynomial { + self + &rhs + } +} + +impl Add for Polynomial { + type Output = Polynomial; + + fn add(self, rhs: Polynomial) -> Polynomial { + &self + &rhs + } +} + +impl<'a, 'b> Add<&'b Polynomial> for &'a Polynomial { + type Output = Polynomial; + + fn add(self, rhs: &'b Polynomial) -> Self::Output { + let len = self.coefficients.len(); + let rhs_len = rhs.coefficients.len(); + + // to have easier bound checks + if rhs_len > len { + return rhs + self; + } + + // we know len >= rhs_len and hence the output will also be of size len + let mut res = Vec::with_capacity(len); + + for i in 0..len { + if let Some(rhs_coeff) = rhs.coefficients.get(i) { + res.push(self[i] + rhs_coeff) + } else { + res.push(self[i]) + } + } + + Polynomial { coefficients: res } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rand_core::SeedableRng; + + #[test] + fn polynomial_evaluation() { + // y = 42 (it should be 42 regardless of x) + let poly = Polynomial { + coefficients: vec![Scalar::from(42)], + }; + + assert_eq!(Scalar::from(42), poly.evaluate_at(&Scalar::from(1))); + assert_eq!(Scalar::from(42), poly.evaluate_at(&Scalar::from(0))); + assert_eq!(Scalar::from(42), poly.evaluate_at(&Scalar::from(10))); + + // y = x + 10, at x = 2 (exp: 12) + let poly = Polynomial { + coefficients: vec![Scalar::from(10), Scalar::from(1)], + }; + + assert_eq!(Scalar::from(12), poly.evaluate_at(&Scalar::from(2))); + + // y = x^4 - 5x^2 + 2x - 3, at x = 3 (exp: 39) + let poly = Polynomial { + coefficients: vec![ + (-Scalar::from(3)), + Scalar::from(2), + (-Scalar::from(5)), + Scalar::zero(), + Scalar::from(1), + ], + }; + + assert_eq!(Scalar::from(39), poly.evaluate_at(&Scalar::from(3))); + + // empty polynomial + let poly = Polynomial::zero(); + + // should always be 0 + assert_eq!(Scalar::from(0), poly.evaluate_at(&Scalar::from(1))); + assert_eq!(Scalar::from(0), poly.evaluate_at(&Scalar::from(0))); + assert_eq!(Scalar::from(0), poly.evaluate_at(&Scalar::from(10))); + } + + #[test] + fn polynomial_addition() { + let empty = Polynomial::zero(); + let p1 = Polynomial { + coefficients: vec![Scalar::from(1), Scalar::from(2), Scalar::from(3)], + }; + let p2 = Polynomial { + coefficients: vec![Scalar::from(4), Scalar::from(5)], + }; + let expected_sum = Polynomial { + coefficients: vec![Scalar::from(5), Scalar::from(7), Scalar::from(3)], + }; + + assert_eq!(p1, &p1 + &empty); + assert_eq!(p1, empty + &p1); + assert_eq!(expected_sum, &p1 + &p2); + assert_eq!(expected_sum, &p2 + &p1); + } + + #[test] + fn public_coefficients_evaluation() { + // we use the same values as in polynomial evaluation test + + let g2 = G2Projective::generator(); + + // y = 42 (it should be 42 regardless of x) + let coeffs = PublicCoefficients { + coefficients: vec![g2 * Scalar::from(42)], + }; + + assert_eq!(g2 * Scalar::from(42), coeffs.evaluate_at(&Scalar::from(1))); + assert_eq!(g2 * Scalar::from(42), coeffs.evaluate_at(&Scalar::from(0))); + assert_eq!(g2 * Scalar::from(42), coeffs.evaluate_at(&Scalar::from(10))); + + // y = x + 10, at x = 2 (exp: 12) + let poly = PublicCoefficients { + coefficients: vec![g2 * Scalar::from(10), g2 * Scalar::from(1)], + }; + + assert_eq!(g2 * Scalar::from(12), poly.evaluate_at(&Scalar::from(2))); + + // y = x^4 - 5x^2 + 2x - 3, at x = 3 (exp: 39) + let coeffs = PublicCoefficients { + coefficients: vec![ + (-g2 * Scalar::from(3)), + g2 * Scalar::from(2), + (-g2 * Scalar::from(5)), + G2Projective::identity(), + g2 * Scalar::from(1), + ], + }; + + assert_eq!(g2 * Scalar::from(39), coeffs.evaluate_at(&Scalar::from(3))); + + // empty coefficients + let coeffs = PublicCoefficients { + coefficients: Vec::new(), + }; + + // should always be 0 + assert_eq!( + G2Projective::identity(), + coeffs.evaluate_at(&Scalar::from(1)) + ); + assert_eq!( + G2Projective::identity(), + coeffs.evaluate_at(&Scalar::from(0)) + ); + assert_eq!( + G2Projective::identity(), + coeffs.evaluate_at(&Scalar::from(10)) + ); + } + + #[test] + fn public_coefficients_roundtrip() { + let dummy_seed = [1u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + + let good = vec![ + Polynomial::zero().public_coefficients(), + Polynomial::new_random(&mut rng, 0).public_coefficients(), + Polynomial::new_random(&mut rng, 1).public_coefficients(), + Polynomial::new_random(&mut rng, 4).public_coefficients(), + Polynomial::new_random(&mut rng, 15).public_coefficients(), + ]; + + for coefficient in good { + let bytes = coefficient.to_bytes(); + let recovered = PublicCoefficients::try_from_bytes(&bytes).unwrap(); + assert_eq!(coefficient, recovered); + } + + assert!(PublicCoefficients::try_from_bytes(&[]).is_err()); + assert!(PublicCoefficients::try_from_bytes(&[1]).is_err()); + assert!(PublicCoefficients::try_from_bytes(&[1, 2, 3, 4]).is_err()); + + let g2 = G2Projective::generator().to_bytes(); + let mut bad_length = Vec::new(); + bad_length.extend_from_slice(&2u32.to_be_bytes()); + bad_length.extend_from_slice(g2.as_ref()); + assert!(PublicCoefficients::try_from_bytes(&bad_length).is_err()); + + let mut incomplete = Vec::new(); + incomplete.extend_from_slice(&1u32.to_be_bytes()); + incomplete.extend_from_slice(&g2.as_ref()[..95]); + assert!(PublicCoefficients::try_from_bytes(&incomplete).is_err()); + } +} diff --git a/common/crypto/dkg/src/lib.rs b/common/crypto/dkg/src/lib.rs new file mode 100644 index 0000000000..f754b16153 --- /dev/null +++ b/common/crypto/dkg/src/lib.rs @@ -0,0 +1,95 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +// forward-secure public key encryption scheme +pub mod bte; +pub mod error; +pub mod interpolation; + +// this entire module is a big placeholder for whatever scheme we decide to use for the +// secure channel encryption scheme, but I would assume that the top-level API would +// remain more or less the same +pub mod dealing; +pub(crate) mod share; +pub(crate) mod utils; + +pub use dealing::*; +pub use share::*; + +// TODO: presumably this should live in a some different, common, crate? +pub type Threshold = u64; +pub type NodeIndex = u64; + +#[cfg(test)] +mod tests { + use crate::interpolation::perform_lagrangian_interpolation_at_origin; + use crate::interpolation::polynomial::Polynomial; + use bls12_381::Scalar; + use rand_chacha::rand_core::SeedableRng; + + #[test] + fn basic_dummy_secret_sharing() { + let degree = 2; + + let dummy_seed = [1u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + + let p1 = Polynomial::new_random(&mut rng, degree); + let p2 = Polynomial::new_random(&mut rng, degree); + let p3 = Polynomial::new_random(&mut rng, degree); + let p4 = Polynomial::new_random(&mut rng, degree); + + let zero = Scalar::zero(); + let one = Scalar::one(); + let two = Scalar::from(2); + let three = Scalar::from(3); + let four = Scalar::from(4); + + // i.e. given: + // p1 = a1 + x * b1 + ... + // p2 = a2 + x * b2 + ... + // ... + // expected = (a1 + a2 + ...) + x * (b1 + b2 + ...) + ... + // note: master polynomial is NEVER explicitly computed + let expected_master = &p1 + &p2 + &p3 + &p4; + + let v1_secret = p1.evaluate_at(&one) + + p2.evaluate_at(&one) + + p3.evaluate_at(&one) + + p4.evaluate_at(&one); + let v2_secret = p1.evaluate_at(&two) + + p2.evaluate_at(&two) + + p3.evaluate_at(&two) + + p4.evaluate_at(&two); + let v3_secret = p1.evaluate_at(&three) + + p2.evaluate_at(&three) + + p3.evaluate_at(&three) + + p4.evaluate_at(&three); + let v4_secret = p1.evaluate_at(&four) + + p2.evaluate_at(&four) + + p3.evaluate_at(&four) + + p4.evaluate_at(&four); + + // note that the following would have never happened in actual dkg setting, but it's + // used here mostly for a sanity check on the maths used + let samples = vec![ + (one, v1_secret), + (two, v2_secret), + (three, v3_secret), + (four, v4_secret), + ]; + let master_secret = perform_lagrangian_interpolation_at_origin(&samples).unwrap(); + + assert_eq!(expected_master.evaluate_at(&zero), master_secret); + assert_eq!(expected_master.evaluate_at(&one), v1_secret); + assert_eq!(expected_master.evaluate_at(&two), v2_secret); + assert_eq!(expected_master.evaluate_at(&three), v3_secret); + assert_eq!(expected_master.evaluate_at(&four), v4_secret); + + // since we have 4 parties, but polynomials used are of degree 2, we only need at least 3 + // issuers to contribute + let samples2 = vec![(one, v1_secret), (three, v3_secret), (four, v4_secret)]; + let master_secret2 = perform_lagrangian_interpolation_at_origin(&samples2).unwrap(); + assert_eq!(master_secret, master_secret2) + } +} diff --git a/common/crypto/dkg/src/share.rs b/common/crypto/dkg/src/share.rs new file mode 100644 index 0000000000..a108a064ae --- /dev/null +++ b/common/crypto/dkg/src/share.rs @@ -0,0 +1,130 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::bte::{CHUNK_BYTES, NUM_CHUNKS, SCALAR_SIZE}; +use crate::error::DkgError; +use crate::interpolation::perform_lagrangian_interpolation_at_origin; +use crate::NodeIndex; +use bls12_381::Scalar; +use zeroize::Zeroize; + +// if this type is changed, one must ensure all values can fit in it +pub type Chunk = u16; + +#[derive(PartialEq, Eq, Debug, Zeroize)] +#[cfg_attr(test, derive(Clone))] +#[zeroize(drop)] +pub struct Share(pub(crate) Scalar); + +pub fn combine_shares(shares: Vec, node_indices: &[NodeIndex]) -> Result { + if shares.len() != node_indices.len() { + return Err(DkgError::MismatchedLagrangianSamplesLengths { + x: node_indices.len(), + y: shares.len(), + }); + } + + let samples = shares + .into_iter() + .zip(node_indices.iter()) + .map(|(share, index)| (Scalar::from(*index), share.0)) + .collect::>(); + + perform_lagrangian_interpolation_at_origin(&samples) +} + +impl Share { + // not really used outside tests + #[cfg(test)] + pub(crate) fn random(mut rng: impl rand_core::RngCore) -> Self { + use ff::Field; + Share(Scalar::random(&mut rng)) + } + + pub(crate) fn to_chunks(&self) -> ChunkedShare { + let mut chunks = [0; NUM_CHUNKS]; + let mut bytes = self.0.to_bytes(); + + for (chunk, chunk_bytes) in chunks.iter_mut().zip(bytes[..].chunks_exact(CHUNK_BYTES)) { + let mut tmp = [0u8; CHUNK_BYTES]; + tmp.copy_from_slice(chunk_bytes); + *chunk = Chunk::from_le_bytes(tmp) + } + + bytes.zeroize(); + ChunkedShare { chunks } + } + + pub(crate) fn inner(&self) -> &Scalar { + &self.0 + } +} + +impl From for Share { + fn from(s: Scalar) -> Self { + Share(s) + } +} + +#[derive(Default, Zeroize)] +#[cfg_attr(test, derive(Clone))] +#[zeroize(drop)] +pub(crate) struct ChunkedShare { + pub(crate) chunks: [Chunk; NUM_CHUNKS], +} + +impl From for ChunkedShare { + fn from(share: Share) -> ChunkedShare { + share.to_chunks() + } +} + +impl TryFrom for Share { + type Error = DkgError; + + fn try_from(chunked: ChunkedShare) -> Result { + let mut bytes = [0u8; SCALAR_SIZE]; + for (chunk, chunk_bytes) in chunked + .chunks + .iter() + .zip(bytes[..].chunks_exact_mut(CHUNK_BYTES)) + { + let tmp = chunk.to_le_bytes(); + chunk_bytes.copy_from_slice(&tmp[..]); + } + + let recovered = Option::from(Scalar::from_bytes(&bytes)) + .map(Share) + .ok_or(DkgError::MalformedShare)?; + + bytes.zeroize(); + Ok(recovered) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::utils::combine_scalar_chunks; + use rand_core::SeedableRng; + + #[test] + fn chunking_share() { + let dummy_seed = [1u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + + let share = Share::random(&mut rng); + let chunks: ChunkedShare = share.clone().into(); + + let scalar_chunks = chunks + .chunks + .iter() + .map(|c| Scalar::from(*c as u64)) + .collect::>(); + let expected = combine_scalar_chunks(&scalar_chunks); + assert_eq!(expected, share.0); + + let recombined: Share = chunks.try_into().unwrap(); + assert_eq!(expected, recombined.0); + } +} diff --git a/common/crypto/dkg/src/todos.md b/common/crypto/dkg/src/todos.md new file mode 100644 index 0000000000..70a979b057 --- /dev/null +++ b/common/crypto/dkg/src/todos.md @@ -0,0 +1,4 @@ +Just a todo file to keep track of what should be changed + +- Deriving challenges and oracles should be more streamlined; perhaps some trait for hashing +- perhaps there could be additional intermediate types in proofs of knowledge (for moves / responses / etc) diff --git a/common/crypto/dkg/src/utils.rs b/common/crypto/dkg/src/utils.rs new file mode 100644 index 0000000000..28fb99ad5d --- /dev/null +++ b/common/crypto/dkg/src/utils.rs @@ -0,0 +1,114 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::bte::CHUNK_SIZE; +use bls12_381::hash_to_curve::{ExpandMsgXmd, HashToCurve, HashToField}; +use bls12_381::G1Projective; +use bls12_381::{G2Projective, Scalar}; +use group::GroupEncoding; +use sha2::{Digest, Sha256}; + +#[macro_export] +macro_rules! ensure_len { + ($a:expr, $b:expr) => { + if $a.len() != $b { + return false; + } + }; +} + +pub(crate) struct RandomOracleBuilder { + inner_state: Sha256, +} + +impl RandomOracleBuilder { + pub(crate) fn new(domain: &[u8]) -> Self { + let mut inner_state = Sha256::new(); + inner_state.update(domain); + + RandomOracleBuilder { inner_state } + } + + pub(crate) fn update(&mut self, data: impl AsRef<[u8]>) { + self.inner_state.update(data) + } + + pub(crate) fn update_with_g1_elements<'a, I>(&mut self, items: I) + where + I: Iterator, + { + items.for_each(|item| self.update(item.to_bytes())) + } + + pub(crate) fn finalize(self) -> [u8; 32] { + self.inner_state.finalize().into() + } +} + +// those will most likely need to somehow get re-combined with coconut (or maybe extracted to a completely different module) +pub(crate) fn hash_to_scalar>(msg: M, domain: &[u8]) -> Scalar { + // the unwrap here is fine as the result vector will have 1 element (as specified) and will not be empty + hash_to_scalars(msg, domain, 1).pop().unwrap() +} + +pub(crate) fn hash_to_scalars>(msg: M, domain: &[u8], n: usize) -> Vec { + let mut output = vec![Scalar::zero(); n]; + + Scalar::hash_to_field::>(msg.as_ref(), domain, &mut output); + output +} + +pub(crate) fn hash_g2>(msg: M, domain: &[u8]) -> G2Projective { + >>::hash_to_curve(msg, domain) +} + +pub(crate) fn combine_scalar_chunks(chunks: &[Scalar]) -> Scalar { + let chunk_size_scalar = Scalar::from(CHUNK_SIZE as u64); + chunks.iter().rev().fold(Scalar::zero(), |mut acc, chunk| { + acc *= chunk_size_scalar; + acc += chunk; + acc + }) +} + +pub(crate) fn combine_g1_chunks(chunks: &[G1Projective]) -> G1Projective { + let chunk_size_scalar = Scalar::from(CHUNK_SIZE as u64); + chunks + .iter() + .rev() + .fold(G1Projective::identity(), |mut acc, chunk| { + acc *= chunk_size_scalar; + acc += chunk; + acc + }) +} + +pub(crate) fn deserialize_scalar(b: &[u8]) -> Option { + if b.len() != 32 { + None + } else { + let mut repr: [u8; 32] = Default::default(); + repr.as_mut().copy_from_slice(b); + Scalar::from_bytes(&repr).into() + } +} + +pub(crate) fn deserialize_g1(b: &[u8]) -> Option { + if b.len() != 48 { + None + } else { + let mut encoding = ::Repr::default(); + encoding.as_mut().copy_from_slice(b); + G1Projective::from_bytes(&encoding).into() + } +} + +pub(crate) fn deserialize_g2(b: &[u8]) -> Option { + if b.len() != 96 { + None + } else { + let mut encoding = ::Repr::default(); + encoding.as_mut().copy_from_slice(b); + G2Projective::from_bytes(&encoding).into() + } +} diff --git a/common/crypto/dkg/tests/integration.rs b/common/crypto/dkg/tests/integration.rs new file mode 100644 index 0000000000..a7cc9c1bf9 --- /dev/null +++ b/common/crypto/dkg/tests/integration.rs @@ -0,0 +1,285 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use bls12_381::{G2Projective, Scalar}; +use dkg::bte::{decrypt_share, keygen, setup, Epoch}; +use dkg::interpolation::perform_lagrangian_interpolation_at_origin; +use dkg::{combine_shares, try_recover_verification_keys, Dealing}; +use rand_core::SeedableRng; +use std::collections::BTreeMap; + +#[test] +fn single_sender() { + // makes it easier to understand than `full_threshold_secret_sharing` + // and is a good stepping stone, because its everything each node will have to perform (from one point of view) + + let dummy_seed = [42u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + let params = setup(); + + // the simplest possible case + let threshold = 2; + + // the indices are going to get assigned externally, so for test sake, use non-consecutive ones + let node_indices = vec![15u64, 248, 33521]; + + let mut receivers = BTreeMap::new(); + let mut full_keys = Vec::new(); + for index in &node_indices { + let (dk, pk) = keygen(¶ms, &mut rng); + receivers.insert(*index, *pk.public_key()); + full_keys.push((dk, pk)) + } + + // start off in a defined epoch (i.e. not root); + let epoch = Epoch::new(2); + + // TODO: HERE BE SERIALIZATION / DESERIALIZATION THAT'S NOT IMPLEMENTED YET + // verify remote proofs of key possession + for key in full_keys.iter() { + assert!(key.1.verify()); + } + + let (dealing, dealer_share) = Dealing::create( + &mut rng, + ¶ms, + node_indices[0], + threshold, + epoch, + &receivers, + None, + ); + dealing + .verify(¶ms, epoch, threshold, &receivers, None) + .unwrap(); + + // make sure each share is actually decryptable (even though proofs say they must be, perform this sanity check) + for (i, (ref mut dk, _)) in full_keys.iter_mut().enumerate() { + dk.try_update_to(epoch, ¶ms, &mut rng).unwrap(); + let _recovered = decrypt_share(dk, i, &dealing.ciphertexts, epoch, None).unwrap(); + } + + // and for good measure, check that the dealer's share matches decryption result + let recovered_dealer = + decrypt_share(&full_keys[0].0, 0, &dealing.ciphertexts, epoch, None).unwrap(); + assert_eq!(recovered_dealer, dealer_share.unwrap()); +} + +#[test] +fn full_threshold_secret_sharing() { + let dummy_seed = [42u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + let params = setup(); + + // the simplest possible case + let threshold = 2; + + // the indices are going to get assigned externally, so for test sake, use non-consecutive ones + let node_indices = vec![15u64, 248, 33521]; + + let mut receivers = BTreeMap::new(); + let mut full_keys = Vec::new(); + for index in &node_indices { + let (dk, pk) = keygen(¶ms, &mut rng); + receivers.insert(*index, *pk.public_key()); + full_keys.push((dk, pk)) + } + + // start off in a defined epoch (i.e. not root); + let epoch = Epoch::new(2); + + // TODO: HERE BE SERIALIZATION / DESERIALIZATION THAT'S NOT IMPLEMENTED YET + // verify remote proofs of key possession + for key in full_keys.iter() { + assert!(key.1.verify()); + } + + let dealings = node_indices + .iter() + .map(|&dealer_index| { + Dealing::create( + &mut rng, + ¶ms, + dealer_index, + threshold, + epoch, + &receivers, + None, + ) + .0 + }) + .collect::>(); + for dealing in dealings.iter() { + dealing + .verify(¶ms, epoch, threshold, &receivers, None) + .unwrap(); + } + + // recover verification keys + let (recovered_master, recovered_partials) = + try_recover_verification_keys(&dealings, threshold, &receivers).unwrap(); + + let g2 = G2Projective::generator(); + + let mut derived_secrets = Vec::new(); + for (i, (ref mut dk, _)) in full_keys.iter_mut().enumerate() { + dk.try_update_to(epoch, ¶ms, &mut rng).unwrap(); + + let shares = dealings + .iter() + .map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, epoch, None).unwrap()) + .collect(); + + // we know dealer_share matches, but it would be inconvenient to try to put them in here, + // so for ease of use (IN A TEST SETTING), just decrypt one's own share + let recovered_secret = + combine_shares(shares, &receivers.keys().copied().collect::>()).unwrap(); + + // make sure it matches the associated vk + assert_eq!(recovered_partials[i], g2 * recovered_secret); + + derived_secrets.push(recovered_secret) + } + + // sanity check that the shares were combined correctly and if we take threshold number of them, + // we end up with the same master secret, note: those are NEVER explicitly recovered in actual system + // (remember threshold was 2) + let master1 = perform_lagrangian_interpolation_at_origin(&[ + (Scalar::from(node_indices[0]), derived_secrets[0]), + (Scalar::from(node_indices[1]), derived_secrets[1]), + ]) + .unwrap(); + + let master2 = perform_lagrangian_interpolation_at_origin(&[ + (Scalar::from(node_indices[1]), derived_secrets[1]), + (Scalar::from(node_indices[2]), derived_secrets[2]), + ]) + .unwrap(); + + assert_eq!(master1, master2); + assert_eq!(recovered_master, g2 * master1); +} + +#[test] +fn full_threshold_secret_resharing() { + let dummy_seed = [42u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + let params = setup(); + + // the simplest possible case + let threshold = 2; + + // the indices are going to get assigned externally, so for test sake, use non-consecutive ones + let node_indices = vec![15u64, 248, 33521]; + + let mut receivers = BTreeMap::new(); + let mut full_keys = Vec::new(); + for index in &node_indices { + let (dk, pk) = keygen(¶ms, &mut rng); + receivers.insert(*index, *pk.public_key()); + full_keys.push((dk, pk)) + } + + // start off in a defined epoch (i.e. not root); + let epoch = Epoch::new(2); + + let first_dealings = node_indices + .iter() + .map(|&dealer_index| { + Dealing::create( + &mut rng, + ¶ms, + dealer_index, + threshold, + epoch, + &receivers, + None, + ) + .0 + }) + .collect::>(); + + // recover verification keys + let (public_original_master, recovered_partials) = + try_recover_verification_keys(&first_dealings, threshold, &receivers).unwrap(); + + let mut derived_secrets = Vec::new(); + for (i, (ref mut dk, _)) in full_keys.iter_mut().enumerate() { + dk.try_update_to(epoch, ¶ms, &mut rng).unwrap(); + + let shares = first_dealings + .iter() + .map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, epoch, None).unwrap()) + .collect(); + + let recovered_secret = + combine_shares(shares, &receivers.keys().copied().collect::>()).unwrap(); + + derived_secrets.push(recovered_secret) + } + + let original_master = perform_lagrangian_interpolation_at_origin(&[ + (Scalar::from(node_indices[0]), derived_secrets[0]), + (Scalar::from(node_indices[1]), derived_secrets[1]), + ]) + .unwrap(); + + let next_epoch = Epoch::new(3); + + // attempt to create resharing dealings! + let resharing_dealings = node_indices + .iter() + .zip(derived_secrets.iter()) + .map(|(&dealer_index, prior_secret)| { + Dealing::create( + &mut rng, + ¶ms, + dealer_index, + threshold, + next_epoch, + &receivers, + Some(*prior_secret), + ) + .0 + }) + .collect::>(); + + for (reshared_dealing, prior_vk) in resharing_dealings.iter().zip(recovered_partials.iter()) { + reshared_dealing + .verify(¶ms, next_epoch, threshold, &receivers, Some(*prior_vk)) + .unwrap(); + } + + // recover verification keys + let (public_reshared_master, reshared_partials) = + try_recover_verification_keys(&resharing_dealings, threshold, &receivers).unwrap(); + + let mut reshared_secrets = Vec::new(); + for (i, (ref mut dk, _)) in full_keys.iter_mut().enumerate() { + dk.try_update_to(next_epoch, ¶ms, &mut rng).unwrap(); + + let shares = resharing_dealings + .iter() + .map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, next_epoch, None).unwrap()) + .collect(); + + let recovered_secret = + combine_shares(shares, &receivers.keys().copied().collect::>()).unwrap(); + + reshared_secrets.push(recovered_secret) + } + + let reshared_master = perform_lagrangian_interpolation_at_origin(&[ + (Scalar::from(node_indices[0]), reshared_secrets[0]), + (Scalar::from(node_indices[1]), reshared_secrets[1]), + ]) + .unwrap(); + + // the master secret and public values didn't change + assert_eq!(original_master, reshared_master); + assert_eq!(public_original_master, public_reshared_master); + + // but partials did + assert_ne!(derived_secrets, reshared_secrets); + assert_ne!(recovered_partials, reshared_partials); +}