From 0d04a8db558d9f78b4b116df377d25df2af82952 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Fri, 10 Nov 2023 15:31:00 -0800 Subject: [PATCH 1/4] Improve logging in integration tests --- synedrion/Cargo.toml | 1 + synedrion/tests/sessions.rs | 31 ++++++++++++++++++++++++------- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/synedrion/Cargo.toml b/synedrion/Cargo.toml index 9607207b..2734963c 100644 --- a/synedrion/Cargo.toml +++ b/synedrion/Cargo.toml @@ -35,6 +35,7 @@ tokio = { version = "1", features = ["rt", "sync", "time", "macros"] } rand = "0.8" criterion = "0.5" itertools = "0.11" +hex = "0.4" [features] bench-internals = ["itertools"] # makes some internal functions public to allow external benchmarks diff --git a/synedrion/tests/sessions.rs b/synedrion/tests/sessions.rs index 5332f2dd..1ea82083 100644 --- a/synedrion/tests/sessions.rs +++ b/synedrion/tests/sessions.rs @@ -17,6 +17,10 @@ use synedrion::{ type MessageOut = (VerifyingKey, VerifyingKey, SignedMessage); type MessageIn = (VerifyingKey, SignedMessage); +fn key_to_str(key: &VerifyingKey) -> String { + hex::encode(&key.to_encoded_point(true).as_bytes()[1..5]) +} + async fn run_session( tx: mpsc::Sender, rx: mpsc::Receiver, @@ -28,9 +32,13 @@ async fn run_session( let mut cached_messages = Vec::<(VerifyingKey, SignedMessage)>::new(); let key = session.verifier(); + let key_str = key_to_str(&key); loop { - println!("*** {key:?}: starting round {:?}", session.current_round()); + println!( + "{key_str}: *** starting round {:?} ***", + session.current_round() + ); // This is kept in the main task since it's mutable, // and we don't want to bother with synchronization. @@ -45,7 +53,10 @@ async fn run_session( // In production usage, this will happen in a spawned task let message = session.make_broadcast(&mut OsRng).unwrap(); for destination in destinations.iter() { - println!("{key:?}: sending a broadcast to {destination:?}"); + println!( + "{key_str}: sending a broadcast to {}", + key_to_str(destination) + ); tx.send((key, *destination, message.clone())).await.unwrap(); } } @@ -60,7 +71,10 @@ async fn run_session( let (message, artefact) = session .make_direct_message(&mut OsRng, destination) .unwrap(); - println!("{key:?}: sending a direct message to {destination:?}"); + println!( + "{key_str}: sending a direct message to {}", + key_to_str(destination) + ); tx.send((key, *destination, message)).await.unwrap(); // This will happen in a host task @@ -70,7 +84,10 @@ async fn run_session( for (from, message) in cached_messages { // In production usage, this will happen in a spawned task. - println!("{key:?}: applying a cached message from {from:?}"); + println!( + "{key_str}: applying a cached message from {}", + key_to_str(&from) + ); let result = session.verify_message(&from, message).unwrap(); // This will happen in a host task. @@ -78,21 +95,21 @@ async fn run_session( } while !session.can_finalize(&accum).unwrap() { - println!("{key:?}: waiting for a message"); + println!("{key_str}: waiting for a message"); let (from, message) = rx.recv().await.unwrap(); // TODO: check here that the message from this origin hasn't been already processed // if accum.already_processed(message) { ... } // In production usage, this will happen in a spawned task. - println!("{key:?}: applying a message from {from:?}"); + println!("{key_str}: applying a message from {}", key_to_str(&from)); let result = session.verify_message(&from, message).unwrap(); // This will happen in a host task. accum.add_processed_message(result).unwrap().unwrap(); } - println!("{key:?}: finalizing the round"); + println!("{key_str}: finalizing the round"); match session.finalize_round(&mut OsRng, accum).unwrap() { FinalizeOutcome::Success(res) => break res, From d42ae4944ae6743a5a285c422a348d50cf0e2fb0 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Fri, 10 Nov 2023 15:31:48 -0800 Subject: [PATCH 2/4] Use bincode for serializing messages The built-in serialization of Uints has problems with MessagePack, producing variable-sized serialization. --- synedrion/Cargo.toml | 2 +- synedrion/src/sessions/type_erased.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/synedrion/Cargo.toml b/synedrion/Cargo.toml index 2734963c..f8ae6622 100644 --- a/synedrion/Cargo.toml +++ b/synedrion/Cargo.toml @@ -23,7 +23,7 @@ crypto-bigint = { version = "0.5.3", features = ["serde"] } crypto-primes = "0.5" serde = { version = "1", features = ["derive"] } -rmp-serde = "1" +bincode = "1" cfg-if = "1" itertools = { version = "0.11", default-features = false, optional = true } diff --git a/synedrion/src/sessions/type_erased.rs b/synedrion/src/sessions/type_erased.rs index 2bb37f4a..9a255f02 100644 --- a/synedrion/src/sessions/type_erased.rs +++ b/synedrion/src/sessions/type_erased.rs @@ -19,7 +19,7 @@ use crate::cggmp21::{ use crate::tools::collections::{HoleRange, HoleVec, HoleVecAccum}; pub(crate) fn serialize_message(message: &impl Serialize) -> Result, LocalError> { - rmp_serde::encode::to_vec(message) + bincode::serialize(message) .map(|serialized| serialized.into_boxed_slice()) .map_err(|err| LocalError(format!("Failed to serialize: {err:?}"))) } @@ -27,7 +27,7 @@ pub(crate) fn serialize_message(message: &impl Serialize) -> Result, L pub(crate) fn deserialize_message Deserialize<'de>>( message_bytes: &[u8], ) -> Result { - rmp_serde::decode::from_slice(message_bytes).map_err(|err| err.to_string()) + bincode::deserialize(message_bytes).map_err(|err| err.to_string()) } pub(crate) enum FinalizeOutcome { From c134c76240827c6b3c0aaae758c88a0aec1d7109 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Sat, 11 Nov 2023 11:32:11 -0800 Subject: [PATCH 3/4] Pack Signed integers before serializing --- synedrion/src/uint/signed.rs | 104 +++++++++++++++++------------------ synedrion/src/uint/traits.rs | 1 + 2 files changed, 53 insertions(+), 52 deletions(-) diff --git a/synedrion/src/uint/signed.rs b/synedrion/src/uint/signed.rs index 68a0d531..20e2b198 100644 --- a/synedrion/src/uint/signed.rs +++ b/synedrion/src/uint/signed.rs @@ -1,74 +1,74 @@ -use core::fmt; -use core::marker::PhantomData; +use alloc::boxed::Box; +use alloc::format; +use alloc::string::String; use core::ops::{Add, Mul, Neg, Not, Sub}; use digest::XofReader; use rand_core::CryptoRngCore; -use serde::{ - de, de::Error, ser::SerializeTupleStruct, Deserialize, Deserializer, Serialize, Serializer, -}; +use serde::{Deserialize, Serialize}; use super::{ subtle::{Choice, ConditionallyNegatable, ConditionallySelectable, ConstantTimeEq, CtOption}, Bounded, CheckedAdd, CheckedMul, FromScalar, HasWide, Integer, NonZero, UintLike, UintModLike, }; use crate::curve::{Scalar, ORDER}; - -/// A wrapper over unsigned integers that treats two's complement numbers as negative. -// In principle, Bounded could be separate from Signed, but we only use it internally, -// and pretty much every time we need a bounded value, it's also signed. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct Signed { - /// bound on the bit size of the absolute value +use crate::tools::serde_bytes; + +/// A packed representation for serializing Signed objects. +/// Usually they have the bound much lower than the full size of the integer, +/// so thiw way we avoid serializing a bunch of zeros. +#[derive(Serialize, Deserialize)] +struct PackedSigned { + is_negative: bool, bound: u32, - value: T, + #[serde(with = "serde_bytes::as_hex")] + bytes: Box<[u8]>, } -impl<'de, T: UintLike + Deserialize<'de>> Deserialize<'de> for Signed { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - struct SignedVisitor(PhantomData); - - impl<'de, T: UintLike + Deserialize<'de>> de::Visitor<'de> for SignedVisitor { - type Value = Signed; - - fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str("a tuple struct Signed") - } +impl From> for PackedSigned { + fn from(val: Signed) -> Self { + let repr = val.abs().to_be_bytes(); + let bound_bytes = (val.bound() + 7) / 8; + let slice = &repr.as_ref()[(repr.as_ref().len() - bound_bytes as usize)..]; + Self { + is_negative: val.is_negative().into(), + bound: val.bound(), + bytes: slice.into(), + } + } +} - fn visit_seq(self, mut seq: A) -> Result, A::Error> - where - A: de::SeqAccess<'de>, - { - let bound: u32 = seq - .next_element()? - .ok_or_else(|| de::Error::invalid_length(0, &self))?; - let value: T = seq - .next_element()? - .ok_or_else(|| de::Error::invalid_length(1, &self))?; - - Signed::new_from_unsigned(value, bound) - .ok_or_else(|| A::Error::custom("The integer is over the declared bound")) - } +impl TryFrom for Signed { + type Error = String; + fn try_from(val: PackedSigned) -> Result { + let mut repr = T::ZERO.to_be_bytes(); + let bytes_len: usize = val.bytes.len(); + let repr_len: usize = repr.as_ref().len(); + + if repr_len < bytes_len { + return Err(format!( + "The bytestring of length {} does not fit the expected integer size {}", + bytes_len, repr_len + )); } - deserializer.deserialize_tuple_struct("Signed", 2, SignedVisitor::(PhantomData)) + repr.as_mut()[(repr_len - bytes_len)..].copy_from_slice(&val.bytes); + let abs_value = T::from_be_bytes(repr); + + Self::new_from_abs(abs_value, val.bound, Choice::from(val.is_negative as u8)) + .ok_or_else(|| "Invalid values for the signed integer".into()) } } -impl Serialize for Signed { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - // TODO: save just the `bound` bytes? That will save some bandwidth. - let mut ts = serializer.serialize_tuple_struct("Signed", 2)?; - ts.serialize_field(&self.bound)?; - ts.serialize_field(&self.value)?; - ts.end() - } +/// A wrapper over unsigned integers that treats two's complement numbers as negative. +// In principle, Bounded could be separate from Signed, but we only use it internally, +// and pretty much every time we need a bounded value, it's also signed. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(try_from = "PackedSigned", into = "PackedSigned")] +pub struct Signed { + /// bound on the bit size of the absolute value + bound: u32, + value: T, } impl Signed { diff --git a/synedrion/src/uint/traits.rs b/synedrion/src/uint/traits.rs index 761b85df..ffd3abb6 100644 --- a/synedrion/src/uint/traits.rs +++ b/synedrion/src/uint/traits.rs @@ -38,6 +38,7 @@ pub(crate) const fn upcast_uint(value: Uint Date: Sat, 11 Nov 2023 12:16:54 -0800 Subject: [PATCH 4/4] Remove a bound variation in `random_bounded_bits_scaled()` Helps with keeping the message sizes constant. --- synedrion/src/cggmp21/sigma/aff_g.rs | 2 +- synedrion/src/cggmp21/sigma/dec.rs | 2 +- synedrion/src/cggmp21/sigma/enc.rs | 2 +- synedrion/src/cggmp21/sigma/fac.rs | 9 +++++---- synedrion/src/cggmp21/sigma/log_star.rs | 2 +- synedrion/src/cggmp21/sigma/mul_star.rs | 2 +- synedrion/src/uint/bounded.rs | 8 ++++++++ synedrion/src/uint/signed.rs | 10 +++++----- 8 files changed, 23 insertions(+), 14 deletions(-) diff --git a/synedrion/src/cggmp21/sigma/aff_g.rs b/synedrion/src/cggmp21/sigma/aff_g.rs index 6c96e47e..cd35c172 100644 --- a/synedrion/src/cggmp21/sigma/aff_g.rs +++ b/synedrion/src/cggmp21/sigma/aff_g.rs @@ -54,7 +54,7 @@ impl AffGProof

{ Signed::from_xof_reader_bounded(&mut reader, &NonZero::new(P::CURVE_ORDER).unwrap()); let e_wide = e.into_wide(); - let hat_cap_n = &aux_rp.public_key().modulus_nonzero(); + let hat_cap_n = &aux_rp.public_key().modulus_bounded(); let alpha = Signed::random_bounded_bits(rng, P::L_BOUND + P::EPS_BOUND); let beta = Signed::random_bounded_bits(rng, P::LP_BOUND + P::EPS_BOUND); diff --git a/synedrion/src/cggmp21/sigma/dec.rs b/synedrion/src/cggmp21/sigma/dec.rs index 7e133ca4..41e5e5b4 100644 --- a/synedrion/src/cggmp21/sigma/dec.rs +++ b/synedrion/src/cggmp21/sigma/dec.rs @@ -42,7 +42,7 @@ impl DecProof

{ let e = Signed::from_xof_reader_bounded(&mut reader, &NonZero::new(P::CURVE_ORDER).unwrap()); - let hat_cap_n = &aux_rp.public_key().modulus_nonzero(); // $\hat{N}$ + let hat_cap_n = &aux_rp.public_key().modulus_bounded(); // $\hat{N}$ let alpha = Signed::random_bounded_bits(rng, P::L_BOUND + P::EPS_BOUND); let mu = Signed::random_bounded_bits_scaled(rng, P::L_BOUND, hat_cap_n); diff --git a/synedrion/src/cggmp21/sigma/enc.rs b/synedrion/src/cggmp21/sigma/enc.rs index a470111d..ff7494a5 100644 --- a/synedrion/src/cggmp21/sigma/enc.rs +++ b/synedrion/src/cggmp21/sigma/enc.rs @@ -41,7 +41,7 @@ impl EncProof

{ Signed::from_xof_reader_bounded(&mut reader, &NonZero::new(P::CURVE_ORDER).unwrap()); let pk = sk.public_key(); - let hat_cap_n = &aux_rp.public_key().modulus_nonzero(); // $\hat{N}$ + let hat_cap_n = &aux_rp.public_key().modulus_bounded(); // $\hat{N}$ // CHECK: should we instead sample in range $+- 2^{\ell + \eps} - q 2^\ell$? // This will ensure that the range check on the prover side will pass. diff --git a/synedrion/src/cggmp21/sigma/fac.rs b/synedrion/src/cggmp21/sigma/fac.rs index c0a8aaa5..8b148576 100644 --- a/synedrion/src/cggmp21/sigma/fac.rs +++ b/synedrion/src/cggmp21/sigma/fac.rs @@ -9,7 +9,7 @@ use crate::paillier::{ SecretKeyPaillierPrecomputed, }; use crate::tools::hashing::{Chain, Hashable, XofHash}; -use crate::uint::{HasWide, Integer, NonZero, Signed}; +use crate::uint::{Bounded, Integer, NonZero, Signed}; const HASH_TAG: &[u8] = b"P_fac"; @@ -45,12 +45,13 @@ impl FacProof

{ let e_wide = e.into_wide(); let pk = sk.public_key(); - let hat_cap_n = &aux_rp.public_key().modulus_nonzero(); // $\hat{N}$ + let hat_cap_n = &aux_rp.public_key().modulus_bounded(); // $\hat{N}$ // CHECK: using `2^(Paillier::PRIME_BITS - 1)` as $\sqrt{N_0}$ (which is its lower bound) - let sqrt_cap_n = NonZero::new( + let sqrt_cap_n = Bounded::new( ::Uint::ONE << (::PRIME_BITS - 1), + ::PRIME_BITS as u32, ) .unwrap(); @@ -60,7 +61,7 @@ impl FacProof

{ let nu = Signed::random_bounded_bits_scaled(rng, P::L_BOUND, hat_cap_n); // N_0 \hat{N} - let scale = NonZero::new(pk.modulus().mul_wide(hat_cap_n.as_ref())).unwrap(); + let scale = pk.modulus_bounded().mul_wide(hat_cap_n); let sigma = Signed::<::Uint>::random_bounded_bits_scaled_wide( diff --git a/synedrion/src/cggmp21/sigma/log_star.rs b/synedrion/src/cggmp21/sigma/log_star.rs index 208fe3d7..2ef9f1bf 100644 --- a/synedrion/src/cggmp21/sigma/log_star.rs +++ b/synedrion/src/cggmp21/sigma/log_star.rs @@ -44,7 +44,7 @@ impl LogStarProof

{ let e = Signed::from_xof_reader_bounded(&mut reader, &NonZero::new(P::CURVE_ORDER).unwrap()); - let hat_cap_n = &aux_rp.public_key().modulus_nonzero(); // $\hat{N}$ + let hat_cap_n = &aux_rp.public_key().modulus_bounded(); // $\hat{N}$ let alpha = Signed::random_bounded_bits(rng, P::L_BOUND + P::EPS_BOUND); let mu = Signed::random_bounded_bits_scaled(rng, P::L_BOUND, hat_cap_n); diff --git a/synedrion/src/cggmp21/sigma/mul_star.rs b/synedrion/src/cggmp21/sigma/mul_star.rs index 6a19fb1a..75c0ce0a 100644 --- a/synedrion/src/cggmp21/sigma/mul_star.rs +++ b/synedrion/src/cggmp21/sigma/mul_star.rs @@ -54,7 +54,7 @@ impl MulStarProof

{ let e = Signed::from_xof_reader_bounded(&mut reader, &NonZero::new(P::CURVE_ORDER).unwrap()); - let hat_cap_n = &aux_rp.public_key().modulus_nonzero(); // $\hat{N}$ + let hat_cap_n = &aux_rp.public_key().modulus_bounded(); // $\hat{N}$ let r = RandomizerMod::random(rng, pk); let alpha = Signed::random_bounded_bits(rng, P::L_BOUND + P::EPS_BOUND); diff --git a/synedrion/src/uint/bounded.rs b/synedrion/src/uint/bounded.rs index c9ea2be3..0668dc0f 100644 --- a/synedrion/src/uint/bounded.rs +++ b/synedrion/src/uint/bounded.rs @@ -116,6 +116,14 @@ impl Bounded { bound: self.bound, } } + + pub fn mul_wide(&self, rhs: &Self) -> Bounded { + let result = self.value.mul_wide(&rhs.value); + Bounded { + value: result, + bound: self.bound + rhs.bound, + } + } } impl FromScalar for Bounded { diff --git a/synedrion/src/uint/signed.rs b/synedrion/src/uint/signed.rs index 20e2b198..acad6f96 100644 --- a/synedrion/src/uint/signed.rs +++ b/synedrion/src/uint/signed.rs @@ -258,7 +258,7 @@ impl Signed { pub fn random_bounded_bits_scaled( rng: &mut impl CryptoRngCore, bound_bits: usize, - scale: &NonZero, + scale: &Bounded, ) -> Signed { assert!(bound_bits < ::BITS - 1); let bound = T::ONE.shl_vartime(bound_bits); @@ -269,7 +269,7 @@ impl Signed { let scaled_bound = scale.as_ref().into_wide().shl_vartime(bound_bits); Signed { - bound: (bound_bits + scale.bits_vartime()) as u32, + bound: bound_bits as u32 + scale.bound(), value: scaled_positive_result.wrapping_sub(&scaled_bound), } } @@ -300,7 +300,7 @@ where pub fn random_bounded_bits_scaled_wide( rng: &mut impl CryptoRngCore, bound_bits: usize, - scale: &NonZero<::Wide>, + scale: &Bounded<::Wide>, ) -> Signed<<::Wide as HasWide>::Wide> { assert!(bound_bits < ::BITS - 1); let bound = T::ONE.shl_vartime(bound_bits); @@ -309,11 +309,11 @@ where let positive_result = positive_result.into_wide(); - let scaled_positive_result = positive_result.mul_wide(scale); + let scaled_positive_result = positive_result.mul_wide(scale.as_ref()); let scaled_bound = scale.as_ref().into_wide().shl_vartime(bound_bits); Signed { - bound: (bound_bits + scale.bits_vartime()) as u32, + bound: bound_bits as u32 + scale.bound(), value: scaled_positive_result.wrapping_sub(&scaled_bound), } }