diff --git a/ampc-actor-utils/src/network/mpc/handle/mod.rs b/ampc-actor-utils/src/network/mpc/handle/mod.rs index cfab007..60df9af 100644 --- a/ampc-actor-utils/src/network/mpc/handle/mod.rs +++ b/ampc-actor-utils/src/network/mpc/handle/mod.rs @@ -17,7 +17,7 @@ use crate::network::mpc::handle::control_channel::{ControlChannel, MeshControlCh use crate::network::tcp::connection::client::{BoxTcpClient, TcpClient, TlsClient}; use crate::network::tcp::connection::server::{BoxTcpServer, TcpServer, TlsServer}; use crate::network::tcp::{self, TcpStreamConn, TlsClientConfig, TlsConfig, TlsServerConfig}; -use crate::protocol::prf::ORBIT5_PARTY_COUNT; +use ampc_secret_sharing::shares::rss5::ORBIT5_PARTY_COUNT; use async_trait::async_trait; use eyre::Result; use itertools::izip; @@ -81,7 +81,7 @@ pub async fn build_network_handle( let identities = match args.addresses.len() { 3 => generate_local_identities(), - n if n == ORBIT5_PARTY_COUNT as usize => generate_local_identities_orbit5(), + n if n == ORBIT5_PARTY_COUNT => generate_local_identities_orbit5(), n => eyre::bail!("unsupported party count {n}: expected 3 or {ORBIT5_PARTY_COUNT}"), }; let role_assignments: RoleAssignment = identities diff --git a/ampc-actor-utils/src/protocol/ops.rs b/ampc-actor-utils/src/protocol/ops.rs index c2169f0..6303d27 100644 --- a/ampc-actor-utils/src/protocol/ops.rs +++ b/ampc-actor-utils/src/protocol/ops.rs @@ -6,9 +6,10 @@ use crate::execution::session::{NetworkSession, Session, SessionHandles}; use crate::network::mpc::{NetworkInt, NetworkValue}; use crate::protocol::binary::{bit_inject, extract_msb_batch, lift, lift_to_ring48, open_bin}; use crate::protocol::prf::{ - orbit5_roles, PairwisePrfKeys, PartyPair, Prf, PrfSeed, ThresholdPrfKeys, ORBIT5_PARTY_COUNT, + orbit5_roles, PairwisePrfKeys, PartyPair, Prf, PrfSeed, ThresholdPrfKeys, }; use ampc_secret_sharing::shares::bit::Bit; +use ampc_secret_sharing::shares::rss5::ORBIT5_PARTY_COUNT; use ampc_secret_sharing::shares::share::DistanceShare; use ampc_secret_sharing::shares::RingRandFillable; use ampc_secret_sharing::shares::{ @@ -151,7 +152,7 @@ fn decode_prf_seed(msg: Result, from: Role) -> Result { pub async fn setup_threshold_prf_keys(session: &mut NetworkSession) -> Result { let own_role = session.own_role(); let num_parties = session.role_assignments.len(); - if num_parties != ORBIT5_PARTY_COUNT as usize { + if num_parties != ORBIT5_PARTY_COUNT { bail!( "threshold PRF key setup requires exactly {ORBIT5_PARTY_COUNT} parties, found {num_parties}" ); @@ -203,7 +204,7 @@ pub async fn setup_threshold_prf_keys(session: &mut NetworkSession) -> Result Result { let own_role = session.own_role(); let num_parties = session.role_assignments.len(); - if num_parties != ORBIT5_PARTY_COUNT as usize { + if num_parties != ORBIT5_PARTY_COUNT { bail!( "pairwise PRF key setup requires exactly {ORBIT5_PARTY_COUNT} parties, found {num_parties}" ); @@ -845,7 +846,7 @@ mod tests { let mut seeds = Vec::new(); for i in 0..ORBIT5_PARTY_COUNT { let mut seed = [0_u8; 16]; - seed[0] = i; + seed[0] = i as u8; seeds.push(seed); } let runtime = LocalRuntime::new(identities.clone(), seeds.clone()) @@ -877,7 +878,7 @@ mod tests { // Every threshold key must be agreed identically by all three owners. for a in 0..ORBIT5_PARTY_COUNT { for b in (a + 1)..ORBIT5_PARTY_COUNT { - let (role_a, role_b) = (Role::new(a as usize), Role::new(b as usize)); + let (role_a, role_b) = (Role::new(a), Role::new(b)); let mut agreed_value: Option = None; let mut num_owners = 0; for (threshold, _) in by_role.iter_mut() { @@ -898,10 +899,10 @@ mod tests { // Every pairwise key must be agreed identically by both parties. for a in 0..ORBIT5_PARTY_COUNT { for b in (a + 1)..ORBIT5_PARTY_COUNT { - let role_a = Role::new(a as usize); - let role_b = Role::new(b as usize); - let a_value = by_role[a as usize].1.get_mut(role_b).unwrap().next_u64(); - let b_value = by_role[b as usize].1.get_mut(role_a).unwrap().next_u64(); + let role_a = Role::new(a); + let role_b = Role::new(b); + let a_value = by_role[a].1.get_mut(role_b).unwrap().next_u64(); + let b_value = by_role[b].1.get_mut(role_a).unwrap().next_u64(); assert_eq!(a_value, b_value); } } diff --git a/ampc-actor-utils/src/protocol/prf.rs b/ampc-actor-utils/src/protocol/prf.rs index 1f0835c..6a3497b 100644 --- a/ampc-actor-utils/src/protocol/prf.rs +++ b/ampc-actor-utils/src/protocol/prf.rs @@ -3,6 +3,7 @@ use crate::protocol::shuffle::Permutation; use ampc_secret_sharing::shares::{ int_ring::IntRing2k, ring_impl::{RingElement, RingRandFillable, VecRingElement}, + rss5::ORBIT5_PARTY_COUNT, }; use eyre::{bail, Result}; use rand::{distributions::Standard, prelude::Distribution, Rng, SeedableRng}; @@ -170,12 +171,8 @@ fn seed_to_rng(seed: PrfSeed) -> PrfRng { } } -/// Number of parties in the ORBIT5 (5-party) protocol configuration used by -/// [`ThresholdPrfKeys`] and [`PairwisePrfKeys`]. -pub const ORBIT5_PARTY_COUNT: u8 = 5; - /// The roles of all ORBIT5 parties, in index order. -pub fn orbit5_roles() -> [Role; ORBIT5_PARTY_COUNT as usize] { +pub fn orbit5_roles() -> [Role; ORBIT5_PARTY_COUNT] { std::array::from_fn(Role::new) } diff --git a/ampc-secret-sharing/src/shares/mod.rs b/ampc-secret-sharing/src/shares/mod.rs index 6b0cfb4..6ceb2b5 100644 --- a/ampc-secret-sharing/src/shares/mod.rs +++ b/ampc-secret-sharing/src/shares/mod.rs @@ -2,6 +2,7 @@ pub mod bit; pub mod int_ring; pub mod ring48; pub mod ring_impl; +pub mod rss5; pub mod share; pub mod vecshare; pub mod vecshare_bittranspose; diff --git a/ampc-secret-sharing/src/shares/rss5.rs b/ampc-secret-sharing/src/shares/rss5.rs new file mode 100644 index 0000000..6e511b5 --- /dev/null +++ b/ampc-secret-sharing/src/shares/rss5.rs @@ -0,0 +1,298 @@ +// 3-of-5 replicated secret sharing, the 5-party analogue of the 3-party `Share` +// +// A secret x is split into ten additive shares indexed by the ten unordered +// pairs of distinct parties, there are 4 choose 2 = 6 such pairs per party. +// Each party holds the 6 shares that do not include its own index. + +use super::{int_ring::IntRing2k, ring_impl::RingElement}; +use num_traits::Zero; +use std::ops::{Add, Mul, Sub}; + +/// Number of parties in the ORBIT5 (5-party) protocol configuration. +pub const ORBIT5_PARTY_COUNT: usize = 5; + +/// Number of shares held by each party: `C(4, 2)`. +pub const RSS5_SLOTS_HELD: usize = 6; + +/// The index pair of each locally-held slot, as offsets from the holder's own +/// role. Slot `i` of party `p` is the share indexed by the pair +/// `{p + SLOT_OFFSETS[i].0, p + SLOT_OFFSETS[i].1}` (mod [`ORBIT5_PARTY_COUNT`]). +/// In particular: +/// Role 0: [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)] +/// Role 1: [(2, 3), (2, 4), (0, 2), (3, 4), (0, 3), (0, 4)] +/// Role 2: [(3, 4), (0, 3), (1, 3), (0, 4), (1, 4), (0, 1)] +/// Role 3: [(0, 4), (1, 4), (2, 4), (0, 1), (0, 2), (1, 2)] +/// Role 4: [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)] +/// +/// See Appedix C of "Multi-Party Replicated Secret Sharing over a Ring with +/// Applications to Privacy-Preserving Machine Learning" by Baccarini, Blanton and Yuan, +/// for the mapping below +pub const SLOT_OFFSETS: [(usize, usize); RSS5_SLOTS_HELD] = + [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]; + +/// The index pair of slot `slot` as held by party `role`, as an +/// ordered pair of absolute role indices. +pub fn slot_pair(role: usize, slot: usize) -> (usize, usize) { + let (i, j) = SLOT_OFFSETS[slot]; + let (i, j) = ( + (role + i) % ORBIT5_PARTY_COUNT, + (role + j) % ORBIT5_PARTY_COUNT, + ); + if i <= j { + (i, j) + } else { + (j, i) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +/// A 3-of-5 replicated share of a value in a ring. +/// The value is shared among five parties, with each party holding six shares. +/// The shares are represented as an array of [RingElement], where `slots[i]` is +/// the share indexed by the party pair `slot_pair(role, i)` and `role` is the +/// holding party. +/// +/// Because the layout is relative to the holder, the same slot index denotes a +/// different share on each party: shares held by different parties must never +/// be combined. +pub struct RssShare { + pub slots: [RingElement; RSS5_SLOTS_HELD], +} + +// Implementations of arithmetic operations for RssShare +impl Add<&Self> for RssShare { + type Output = Self; + + fn add(self, rhs: &Self) -> Self::Output { + RssShare { + slots: std::array::from_fn(|i| self.slots[i] + rhs.slots[i]), + } + } +} + +impl Add for RssShare { + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output { + RssShare { + slots: std::array::from_fn(|i| self.slots[i] + rhs.slots[i]), + } + } +} + +impl Sub for RssShare { + type Output = Self; + + fn sub(self, rhs: Self) -> Self::Output { + RssShare { + slots: std::array::from_fn(|i| self.slots[i] - rhs.slots[i]), + } + } +} + +impl Sub<&Self> for RssShare { + type Output = Self; + + fn sub(self, rhs: &Self) -> Self::Output { + RssShare { + slots: std::array::from_fn(|i| self.slots[i] - rhs.slots[i]), + } + } +} + +/// Multiplication by a public constant, known to every party. +impl Mul for RssShare { + type Output = Self; + + fn mul(self, rhs: T) -> Self::Output { + RssShare { + slots: std::array::from_fn(|i| self.slots[i] * rhs), + } + } +} + +/// Assignment of the 100 cross-terms of a product to the five parties. +/// +/// `MUL_ASSIGN[i]` lists the right-hand slots that a party pairs with its own +/// left-hand slot `i`, so party `p` computes +/// +/// ```ignore +/// v_p = sum over i of ( a_i * sum over j in MUL_ASSIGN[i] of b_j ) +/// ``` +/// +/// Slots are relative to `p`, so every party evaluates the same +/// expression. 20 terms per party cover all `10 * 10` ordered pairs of +/// share indices exactly once across the 5 parties, and every term assigned +/// to `p` uses only slots that `p` holds. +/// +/// Taken from Appendix C of Baccarini, Blanton and Yuan. +const MUL_OPERAND_ASSIGN: [&[usize]; RSS5_SLOTS_HELD] = [ + &[0, 1, 2, 3, 4, 5], // a_0 * (b_0 + b_1 + b_2 + b_3 + b_4 + b_5) + &[0, 1, 2, 3, 4, 5], // a_1 * (b_0 + b_1 + b_2 + b_3 + b_4 + b_5) + &[1, 3], // a_2 * (b_1 + b_3) + &[0, 2], // a_3 * (b_0 + b_2) + &[0, 1], // a_4 * (b_0 + b_1) + &[0, 4], // a_5 * (b_0 + b_4) +]; + +/// The local part of the multiplication +/// [`RssShare`] again. +impl Mul for &RssShare { + type Output = RingElement; + + fn mul(self, rhs: Self) -> Self::Output { + let mut acc = RingElement::zero(); + for (i, rhs_slots) in MUL_OPERAND_ASSIGN.iter().enumerate() { + let mut rhs_sum = RingElement::zero(); + for &j in rhs_slots.iter() { + rhs_sum += rhs.slots[j]; + } + acc += self.slots[i] * rhs_sum; + } + acc + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::shares::bit::Bit; + + use aes_prng::AesRng; + use rand::{Rng, SeedableRng}; + use rand_distr::{Distribution, Standard}; + use std::collections::HashMap; + + /// The ten unordered pairs of distinct parties, i.e. every share index. + fn all_pairs() -> Vec<(usize, usize)> { + (0..ORBIT5_PARTY_COUNT) + .flat_map(|i| (i + 1..ORBIT5_PARTY_COUNT).map(move |j| (i, j))) + .collect() + } + + /// Deal a fresh sharing of `value` and return all five parties' views. + fn get_shares(rng: &mut impl Rng, value: T) -> [RssShare; ORBIT5_PARTY_COUNT] + where + Standard: Distribution, + { + let pairs = all_pairs(); + let (last, rest) = pairs.split_last().unwrap(); + + // Nine uniform shares, and a tenth that makes the ten sum to the secret. + let mut slots: HashMap<(usize, usize), RingElement> = rest + .iter() + .map(|pair| (*pair, RingElement(rng.gen::()))) + .collect(); + let sum = slots + .values() + .fold(RingElement::zero(), |acc, share| acc + *share); + slots.insert(*last, RingElement(value) - sum); + + // Hand each party the six shares whose index pair excludes it. + std::array::from_fn(|role| RssShare { + slots: std::array::from_fn(|slot| slots[&slot_pair(role, slot)]), + }) + } + + /// Reconstruct a secret from all five views, checking on the way that the + /// parties agree on the shares they replicate and that every one of the ten + /// shares is held by somebody. + fn reconstruct_shares( + shares: &[RssShare; ORBIT5_PARTY_COUNT], + ) -> RingElement { + let mut slots: HashMap<(usize, usize), RingElement> = HashMap::new(); + for (role, share) in shares.iter().enumerate() { + for (slot, value) in share.slots.iter().enumerate() { + let pair = slot_pair(role, slot); + if let Some(previous) = slots.insert(pair, *value) { + assert_eq!( + previous, *value, + "parties disagree on the share for {pair:?}" + ); + } + } + } + assert_eq!(slots.len(), all_pairs().len(), "not every share is held"); + + slots + .values() + .fold(RingElement::zero(), |acc, share| acc + *share) + } + + /// Reconstruct from the 5-of-5 additive sharing that the local halves of a + /// multiplication produce. + fn reconstruct_mul_shares( + parts: [RingElement; ORBIT5_PARTY_COUNT], + ) -> RingElement { + parts + .iter() + .fold(RingElement::zero(), |acc, part| acc + *part) + } + + /// [`MUL_OPERAND_ASSIGN`] must partition the cross-terms: each of the 100 ordered + /// pairs of share indices assigned to exactly one party, and no party + /// assigned a term over a share it does not hold. + #[test] + fn mul_assignment_partitions_all_cross_terms() { + type SlotPair = ((usize, usize), (usize, usize)); + let mut assigned_to: HashMap = HashMap::new(); + + for role in 0..ORBIT5_PARTY_COUNT { + for (i, rhs_slots) in MUL_OPERAND_ASSIGN.iter().enumerate() { + for &j in rhs_slots.iter() { + let (lhs, rhs) = (slot_pair(role, i), slot_pair(role, j)); + + // A party can only multiply shares it holds, i.e. shares + // whose index pair excludes it. + for pair in [lhs, rhs] { + assert!( + pair.0 != role && pair.1 != role, + "party {role} is assigned a term over {pair:?}, which it does not hold" + ); + } + + if let Some(other) = assigned_to.insert((lhs, rhs), role) { + panic!("term {lhs:?} * {rhs:?} assigned to both {other} and {role}"); + } + } + } + } + + let pairs = all_pairs(); + assert_eq!( + assigned_to.len(), + pairs.len() * pairs.len(), + "some cross-terms are unassigned" + ); + } + + #[test] + fn mul_matches_plain_multiplication() { + mul_test::(); + mul_test::(); + mul_test::(); + } + fn mul_test() + where + Standard: Distribution, + { + let mut rng = AesRng::from_entropy(); + + for _ in 0..10000 { + let a_t: T = rng.gen(); + let b_t: T = rng.gen(); + + // split a_t and b_t into shares for all five parties + let a = get_shares(&mut rng, a_t); + let b = get_shares(&mut rng, b_t); + + // Dealing and reconstruction agree before any arithmetic happens. + assert_eq!(reconstruct_shares(&a), RingElement(a_t)); + + // Multiplication + let expected_mul = RingElement(a_t.wrapping_mul(&b_t)); + let c: [RingElement; ORBIT5_PARTY_COUNT] = std::array::from_fn(|i| &a[i] * &b[i]); + assert_eq!(reconstruct_mul_shares(c), expected_mul); + } + } +} diff --git a/ampc-secret-sharing/src/shares/share.rs b/ampc-secret-sharing/src/shares/share.rs index 4d86dd2..24c74bd 100644 --- a/ampc-secret-sharing/src/shares/share.rs +++ b/ampc-secret-sharing/src/shares/share.rs @@ -18,7 +18,6 @@ pub trait Role { Clone, Copy, Debug, PartialEq, Default, Eq, PartialOrd, Ord, Serialize, Deserialize, Hash, )] #[serde(bound = "")] -//TODO evagelia: change this for 5pc /// A replicated share of a value in a ring. /// The value is shared among three parties, with each party holding two shares. /// The shares are represented as a pair of [RingElement], where `a` is the share held by party i and `b` is the share held by party i-1 (mod 3). @@ -27,7 +26,6 @@ pub struct Share { pub b: RingElement, } -////TODO evagelia: check operators for 5pc impl Share { pub fn new(a: RingElement, b: RingElement) -> Self { Self { a, b } @@ -38,7 +36,6 @@ impl Share { res.add_assign_const_role(value, role); res } - //TODO evagelia: change this for 5pc pub fn add_assign_const_role(&mut self, other: T, role: R) { match role.index() { 0 => self.a += RingElement(other), @@ -180,7 +177,6 @@ impl MulAssign for Share { self.b *= rhs; } } -//TODO evagelia: change this for 5pc /// This is only the local part of the multiplication (so without randomness and /// without communication)! impl Mul for &Share { @@ -370,7 +366,6 @@ impl Shl for &Share { /// The greater the ratio `code_dot / mask_dot`, the more similar the irises are. #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)] #[serde(bound = "")] -//TODO evagelia: check if this needs changing for 5pc pub struct DistanceShare { pub code_dot: Share, pub mask_dot: Share, diff --git a/ampc-secret-sharing/src/shares/vecshare.rs b/ampc-secret-sharing/src/shares/vecshare.rs index e6245b4..2b0f0e7 100644 --- a/ampc-secret-sharing/src/shares/vecshare.rs +++ b/ampc-secret-sharing/src/shares/vecshare.rs @@ -59,7 +59,7 @@ impl<'a, T: IntRing2k> SliceShareMut<'a, T> { #[derive(Clone, Debug, PartialEq, Default, Eq, PartialOrd, Ord, Serialize, Deserialize)] #[serde(bound = "")] #[repr(transparent)] -////TODO evagelia: check if this needs changing for 5pc +// TODO evagelia: check if this needs changing for 5pc pub struct VecShare { pub shares: Vec>, }