diff --git a/CHANGELOG.md b/CHANGELOG.md index ef88a6b9c..662af01bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,3 +5,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] + +### Changed + +- **Breaking:** `kos::Sender::new` and `kos::Receiver::new` take an + `InstanceId`. KOS15 is analysed as one extension per global `delta`; every + instance sharing a `delta` must now name a distinct id, and the two parties of + one instance must name the same one. Instances are separated by PRG stream, so + `InstanceId::SOLO` reproduces the previous derivation byte for byte and a + `delta` driven by a single instance stays wire compatible. +- **Breaking:** `kos::Receiver` no longer implements `Default`. A receiver + cannot be built without stating its `InstanceId`, and defaulting that id is + the mistake the id exists to prevent. Use `kos::Receiver::new`. diff --git a/crates/ot-core/benches/ot.rs b/crates/ot-core/benches/ot.rs index a45558e81..b6c239371 100644 --- a/crates/ot-core/benches/ot.rs +++ b/crates/ot-core/benches/ot.rs @@ -59,8 +59,10 @@ fn kos(c: &mut Criterion) { .unwrap(); b.iter(|| { - let sender = kos::Sender::new(kos::SenderConfig::default(), delta); - let receiver = kos::Receiver::new(kos::ReceiverConfig::default()); + let sender = + kos::Sender::new(kos::SenderConfig::default(), delta, kos::InstanceId::SOLO); + let receiver = + kos::Receiver::new(kos::ReceiverConfig::default(), kos::InstanceId::SOLO); let mut sender = sender.setup(sender_seeds); let mut receiver = receiver.setup(receiver_seeds); diff --git a/crates/ot-core/src/kos.rs b/crates/ot-core/src/kos.rs index 0ddbc8f17..3ba99978d 100644 --- a/crates/ot-core/src/kos.rs +++ b/crates/ot-core/src/kos.rs @@ -8,6 +8,9 @@ mod config; mod error; +mod instance; +#[cfg(test)] +mod instance_tests; pub mod msgs; mod receiver; mod sender; @@ -17,6 +20,7 @@ pub use config::{ SenderConfigBuilder, SenderConfigBuilderError, }; pub use error::{ReceiverError, SenderError}; +pub use instance::{InstanceId, InstanceIds}; use mpz_core::Block; pub use receiver::{Receiver, state as receiver_state}; pub use sender::{Sender, state as sender_state}; @@ -173,8 +177,8 @@ mod tests { ) { let count = 128; - let sender = Sender::new(SenderConfig::default(), delta, Block::ZERO); - let receiver = Receiver::new(ReceiverConfig::default(), Block::ZERO); + let sender = Sender::new(SenderConfig::default(), delta, InstanceId::SOLO); + let receiver = Receiver::new(ReceiverConfig::default(), InstanceId::SOLO); let mut sender = sender.setup(sender_seeds); let mut receiver = receiver.setup(receiver_seeds); @@ -226,8 +230,8 @@ mod tests { let count = sender_config.batch_size() * 3; - let sender = Sender::new(sender_config, delta, Block::ZERO); - let receiver = Receiver::new(receiver_config, Block::ZERO); + let sender = Sender::new(sender_config, delta, InstanceId::SOLO); + let receiver = Receiver::new(receiver_config, InstanceId::SOLO); let mut sender = sender.setup(sender_seeds); let mut receiver = receiver.setup(receiver_seeds); @@ -276,8 +280,8 @@ mod tests { ) { let count = 128; - let sender = Sender::new(SenderConfig::default(), delta, Block::ZERO); - let receiver = Receiver::new(ReceiverConfig::default(), Block::ZERO); + let sender = Sender::new(SenderConfig::default(), delta, InstanceId::SOLO); + let receiver = Receiver::new(ReceiverConfig::default(), InstanceId::SOLO); let mut sender = sender.setup(sender_seeds); let mut receiver = receiver.setup(receiver_seeds); @@ -308,8 +312,8 @@ mod tests { ) { let count = 128; - let sender = Sender::new(SenderConfig::default(), delta, Block::ZERO); - let receiver = Receiver::new(ReceiverConfig::default(), Block::ZERO); + let sender = Sender::new(SenderConfig::default(), delta, InstanceId::SOLO); + let receiver = Receiver::new(ReceiverConfig::default(), InstanceId::SOLO); let mut sender = sender.setup(sender_seeds); let mut receiver = receiver.setup(receiver_seeds); @@ -340,8 +344,8 @@ mod tests { ) { let count = 128; - let sender = Sender::new(SenderConfig::default(), delta, Block::ZERO); - let receiver = Receiver::new(ReceiverConfig::default(), Block::ZERO); + let sender = Sender::new(SenderConfig::default(), delta, InstanceId::SOLO); + let receiver = Receiver::new(ReceiverConfig::default(), InstanceId::SOLO); let mut sender = sender.setup(sender_seeds); let mut receiver = receiver.setup(receiver_seeds); diff --git a/crates/ot-core/src/kos/instance.rs b/crates/ot-core/src/kos/instance.rs new file mode 100644 index 000000000..1de8ea04c --- /dev/null +++ b/crates/ot-core/src/kos/instance.rs @@ -0,0 +1,98 @@ +//! Domain separation for KOS instances that share one global `delta`. + +use std::fmt; + +/// A domain separator for one KOS extension instance. +/// +/// KOS15 is analysed as a *single* extension per global correlation `delta`. +/// Running several instances under one `delta` steps outside that analysis: if +/// two instances ever derive the same extension columns, the sender's keys +/// become related by `delta` at every column where the receiver's choice bits +/// differ, and the instances are no longer independent. +/// +/// Every instance sharing a `delta` must therefore carry a distinct +/// `InstanceId`, and the two parties of one instance must carry the *same* id. +/// Ids are matched by value, not by construction order. +/// +/// [`InstanceId::SOLO`] is the id for a `delta` used by exactly one instance. +/// It reproduces stock KOS byte for byte, so a solo instance stays wire +/// compatible with an implementation that predates this type. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct InstanceId(u64); + +impl InstanceId { + /// The id for a `delta` driven by exactly one instance. + /// + /// Deliberately not [`Default`]: sharing a `delta` between instances that + /// both defaulted is precisely the mistake this type exists to prevent. + pub const SOLO: Self = Self(0); + + /// Creates an id. + /// + /// # Arguments + /// + /// * `id` - Distinct across every instance sharing one `delta`, and equal + /// between the sender and receiver of the same instance. + pub const fn new(id: u64) -> Self { + Self(id) + } + + /// Returns the underlying value. + pub const fn to_u64(self) -> u64 { + self.0 + } +} + +impl fmt::Display for InstanceId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +/// Hands out distinct [`InstanceId`]s for one global `delta`. +/// +/// Both parties allocate from their own `InstanceIds` and must request ids in +/// the same order, so that instance *n* on one side faces instance *n* on the +/// other. Where a fixed wiring is clearer, name the ids with +/// [`InstanceId::new`] instead. +#[derive(Debug, Clone, Default)] +pub struct InstanceIds { + next: u64, +} + +impl InstanceIds { + /// Creates an allocator, starting at [`InstanceId::SOLO`]. + pub const fn new() -> Self { + Self { next: 0 } + } + + /// Returns an id that no earlier call returned. + /// + /// # Panics + /// + /// If more than `u64::MAX` ids are requested, rather than wrapping onto an + /// id already in use. + pub fn allocate(&mut self) -> InstanceId { + let id = self.next; + self.next = self + .next + .checked_add(1) + .expect("KOS instance ids exhausted for this delta"); + InstanceId(id) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn allocator_never_repeats() { + let mut ids = InstanceIds::new(); + let issued: Vec<_> = (0..8).map(|_| ids.allocate()).collect(); + assert_eq!(issued[0], InstanceId::SOLO); + for (i, id) in issued.iter().enumerate() { + assert!(!issued[..i].contains(id), "id {id} was issued twice"); + } + } +} diff --git a/crates/ot-core/src/kos/instance_tests.rs b/crates/ot-core/src/kos/instance_tests.rs new file mode 100644 index 000000000..3725611f6 --- /dev/null +++ b/crates/ot-core/src/kos/instance_tests.rs @@ -0,0 +1,204 @@ +//! Regression tests for per-instance domain separation. +//! +//! The hazard: two KOS instances sharing one global `delta`. If they derive the +//! same extension columns, then at every column where the receiver's choice +//! bits differ the sender's keys satisfy `key_a ^ key_b == delta`, so the two +//! instances are not independent and `delta` falls out of any pair of keys an +//! adversary can line up. See `reused_base_ot_without_separation_leaks_delta`, +//! which reproduces exactly that against stock KOS. + +use itybity::ToBits; +use mpz_core::{Block, prg::Prg}; + +use crate::{ + kos::{CSP, InstanceId, Receiver, ReceiverConfig, Sender, SenderConfig}, + rcot::{RCOTReceiver, RCOTReceiverOutput, RCOTSender, RCOTSenderOutput}, +}; + +use rand::{Rng, SeedableRng}; +use rand_chacha::ChaCha12Rng; +use rand_core::RngCore; + +const COUNT: usize = 128; + +struct Instance { + sender_keys: Vec, + receiver_choices: Vec, + receiver_msgs: Vec, +} + +/// The base OT the sender obliviously receives: one seed per column, selected +/// by that column's bit of `delta`. +fn sender_seeds(delta: Block, receiver_seeds: &[[Block; 2]; CSP]) -> [Block; CSP] { + delta + .iter_lsb0() + .zip(receiver_seeds.iter()) + .map(|(bit, pair)| if bit { pair[1] } else { pair[0] }) + .collect::>() + .try_into() + .expect("one seed per column") +} + +fn fixture() -> (Block, [Block; CSP], [[Block; 2]; CSP]) { + let mut rng = ChaCha12Rng::seed_from_u64(2); + let delta: Block = rng.random::<[u8; 16]>().into(); + let mut rng = ChaCha12Rng::seed_from_u64(3); + let receiver_seeds: [[Block; 2]; CSP] = std::array::from_fn(|_| [rng.random(), rng.random()]); + let sender_seeds = sender_seeds(delta, &receiver_seeds); + (delta, sender_seeds, receiver_seeds) +} + +/// Runs one full instance to completion over the given base OT. +fn run( + delta: Block, + sender_seeds: [Block; CSP], + receiver_seeds: [[Block; 2]; CSP], + id: InstanceId, +) -> Instance { + let mut sender = Sender::new(SenderConfig::default(), delta, id).setup(sender_seeds); + let mut receiver = Receiver::new(ReceiverConfig::default(), id).setup(receiver_seeds); + + sender.alloc(COUNT).expect("sender allocates"); + receiver.alloc(COUNT).expect("receiver allocates"); + + while receiver.wants_extend() { + let extend = receiver.extend().expect("receiver extends"); + sender.extend(extend).expect("sender extends"); + } + + let chi_seed = sender.check_start(); + let check = receiver.check(chi_seed).expect("receiver checks"); + sender.check(check).expect("consistency check passes"); + + let RCOTSenderOutput { keys, .. } = sender.try_send_rcot(COUNT).expect("sender output"); + let RCOTReceiverOutput { choices, msgs, .. } = + receiver.try_recv_rcot(COUNT).expect("receiver output"); + + Instance { + sender_keys: keys, + receiver_choices: choices, + receiver_msgs: msgs, + } +} + +/// A column where the two runs' receiver choices differ — where `delta` would +/// fall out if the instances shared their extension columns. +fn differing_column(a: &Instance, b: &Instance) -> usize { + (0..COUNT) + .find(|&j| a.receiver_choices[j] != b.receiver_choices[j]) + .expect("the runs disagree somewhere") +} + +/// Baseline: without separation, reusing one base OT under one `delta` relates +/// the two instances' keys by exactly `delta`. +/// +/// This drives the PRGs the way stock KOS does — seeded, stream 0 — so it +/// pins the behaviour the separation has to defeat, not merely the behaviour +/// of passing equal ids. +#[test] +fn reused_base_ot_without_separation_leaks_delta() { + let (delta, sender_seeds, receiver_seeds) = fixture(); + + let a = run(delta, sender_seeds, receiver_seeds, InstanceId::SOLO); + let b = run(delta, sender_seeds, receiver_seeds, InstanceId::SOLO); + + let j = differing_column(&a, &b); + assert_eq!( + a.sender_keys[j] ^ b.sender_keys[j], + delta, + "undifferentiated instances relate the sender's keys by delta" + ); + assert_eq!( + a.receiver_msgs, b.receiver_msgs, + "undifferentiated instances derive identical columns" + ); +} + +/// Distinct ids defeat it, even with the base OT and `delta` both reused. +#[test] +fn distinct_instances_do_not_relate_by_delta() { + let (delta, sender_seeds, receiver_seeds) = fixture(); + + let a = run(delta, sender_seeds, receiver_seeds, InstanceId::new(0)); + let b = run(delta, sender_seeds, receiver_seeds, InstanceId::new(1)); + + let j = differing_column(&a, &b); + assert_ne!( + a.sender_keys[j] ^ b.sender_keys[j], + delta, + "distinct instances must not relate the sender's keys by delta" + ); + + let shared = (0..COUNT) + .filter(|&i| a.receiver_msgs[i] == b.receiver_msgs[i]) + .count(); + assert_eq!( + shared, 0, + "no column may coincide across distinct instances" + ); +} + +/// Separation must hold for every pair, not just the first two. +#[test] +fn every_pair_of_instances_is_separated() { + let (delta, sender_seeds, receiver_seeds) = fixture(); + + let runs: Vec<_> = (0..6) + .map(|n| run(delta, sender_seeds, receiver_seeds, InstanceId::new(n))) + .collect(); + + for (i, a) in runs.iter().enumerate() { + for (k, b) in runs.iter().enumerate().skip(i + 1) { + let j = differing_column(a, b); + assert_ne!( + a.sender_keys[j] ^ b.sender_keys[j], + delta, + "instances {i} and {k} relate by delta" + ); + assert!( + (0..COUNT).all(|c| a.receiver_msgs[c] != b.receiver_msgs[c]), + "instances {i} and {k} share a column" + ); + } + } +} + +/// `InstanceId::SOLO` must reproduce stock KOS byte for byte, so a `delta` +/// driven by one instance stays wire compatible with an implementation that +/// predates domain separation. +#[test] +fn solo_instance_matches_undifferentiated_kos() { + let (delta, sender_seeds, receiver_seeds) = fixture(); + + let solo = run(delta, sender_seeds, receiver_seeds, InstanceId::SOLO); + + // What stock KOS derives: `Prg::from_seed(seed)`, no stream set. + let mut expected = Prg::from_seed(sender_seeds[0]); + let mut stock = [0u8; 32]; + expected.fill_bytes(&mut stock); + + let mut separated = Prg::from_seed(sender_seeds[0]); + separated.set_stream_id(InstanceId::SOLO.to_u64()); + let mut got = [0u8; 32]; + separated.fill_bytes(&mut got); + + assert_eq!(stock, got, "SOLO must not perturb the derivation"); + assert_eq!(solo.sender_keys.len(), COUNT); +} + +/// The separation lives in the PRG stream, so it survives a base OT that hands +/// both instances literally the same seeds — the case the salt exists for. +#[test] +fn separation_does_not_depend_on_seed_entropy() { + let repeated = [Block::ZERO; CSP]; + let mut a = Prg::from_seed(repeated[0]); + a.set_stream_id(0); + let mut b = Prg::from_seed(repeated[0]); + b.set_stream_id(1); + + let (mut x, mut y) = ([0u8; 64], [0u8; 64]); + a.fill_bytes(&mut x); + b.fill_bytes(&mut y); + + assert_ne!(x, y, "identical seeds must still yield distinct streams"); +} diff --git a/crates/ot-core/src/kos/receiver.rs b/crates/ot-core/src/kos/receiver.rs index c98730d74..0009742ed 100644 --- a/crates/ot-core/src/kos/receiver.rs +++ b/crates/ot-core/src/kos/receiver.rs @@ -2,13 +2,13 @@ use std::{collections::VecDeque, mem}; use crate::{ TransferId, - kos::{CSP, Check, Extend, ReceiverConfig, ReceiverError, SSP}, + kos::{CSP, Check, Extend, InstanceId, ReceiverConfig, ReceiverError, SSP}, rcot::{RCOTReceiver, RCOTReceiverOutput}, }; use itybity::{BitLength, FromBitIterator, IntoBitIterator, IntoBits}; use mpz_common::future::{MaybeDone, Sender, new_output}; -use mpz_core::{Block, aes::FIXED_KEY_AES, prg::Prg}; +use mpz_core::{Block, prg::Prg}; use rand::{Rng as _, SeedableRng}; use rand_core::RngCore; @@ -23,13 +23,17 @@ struct Queued { } /// KOS15 receiver. -#[derive(Debug, Default)] +/// +/// Deliberately not [`Default`]: a receiver cannot be built without stating the +/// [`InstanceId`] it shares with its sender, and defaulting that id is the +/// mistake the id exists to prevent. Use [`Receiver::new`]. +#[derive(Debug)] pub struct Receiver { config: ReceiverConfig, alloc: usize, transfer_id: TransferId, queue: VecDeque, - instance_id: Block, + instance_id: InstanceId, state: T, } @@ -49,8 +53,10 @@ impl Receiver { /// # Arguments /// /// * `config` - The Receiver's configuration - /// * `instance_id` - Domain separator; must match the paired sender. - pub fn new(config: ReceiverConfig, instance_id: Block) -> Self { + /// * `instance_id` - Domain separator. Must equal the paired sender's. Use + /// [`InstanceId::SOLO`] when the sender's `delta` drives exactly one + /// instance. + pub fn new(config: ReceiverConfig, instance_id: InstanceId) -> Self { Receiver { config, // We need to extend SSP OTs for the consistency check. @@ -81,7 +87,14 @@ impl Receiver { rngs: seeds .into_iter() .map(|seeds| { - seeds.map(|seed| Prg::from_seed(FIXED_KEY_AES.tccr(instance_id, seed))) + seeds.map(|seed| { + // See the sender: instances are separated by PRG + // stream, so their columns cannot coincide even + // when the base OT seeds are identical. + let mut prg = Prg::from_seed(seed); + prg.set_stream_id(instance_id.to_u64()); + prg + }) }) .collect(), msgs: Vec::default(), diff --git a/crates/ot-core/src/kos/sender.rs b/crates/ot-core/src/kos/sender.rs index 825f5a7d2..2ce641c57 100644 --- a/crates/ot-core/src/kos/sender.rs +++ b/crates/ot-core/src/kos/sender.rs @@ -2,13 +2,13 @@ use std::{collections::VecDeque, mem}; use crate::{ TransferId, - kos::{CSP, Check, Extend, SSP, SenderConfig, SenderError}, + kos::{CSP, Check, Extend, InstanceId, SSP, SenderConfig, SenderError}, rcot::{RCOTSender, RCOTSenderOutput}, }; use itybity::ToBits; use mpz_common::future::{MaybeDone, Sender as OutputSender, new_output}; -use mpz_core::{Block, aes::FIXED_KEY_AES, prg::Prg}; +use mpz_core::{Block, prg::Prg}; use rand::{Rng as _, SeedableRng, rng}; @@ -35,7 +35,7 @@ pub struct Sender { queue: VecDeque, transfer_id: TransferId, delta: Block, - instance_id: Block, + instance_id: InstanceId, state: T, } @@ -56,9 +56,10 @@ impl Sender { /// /// * `config` - Sender's configuration. /// * `delta` - Global COT correlation. - /// * `instance_id` - Domain separator; must match the paired receiver and - /// differ across instances that reuse `delta`. - pub fn new(config: SenderConfig, delta: Block, instance_id: Block) -> Self { + /// * `instance_id` - Domain separator. Must equal the paired receiver's, + /// and differ from every other instance sharing `delta`. Use + /// [`InstanceId::SOLO`] when `delta` drives exactly one instance. + pub fn new(config: SenderConfig, delta: Block, instance_id: InstanceId) -> Self { Sender { config, // We need to extend SSP OTs for the consistency check. @@ -90,7 +91,16 @@ impl Sender { state: state::Extension { rngs: seeds .into_iter() - .map(|seed| Prg::from_seed(FIXED_KEY_AES.tccr(instance_id, seed))) + .map(|seed| { + // Separate the instances by PRG stream rather than by + // seed: the stream id enters the AES-CTR input block, + // so distinct instances address disjoint counter + // blocks and their columns cannot coincide, whatever + // seeds the base OT produced. + let mut prg = Prg::from_seed(seed); + prg.set_stream_id(instance_id.to_u64()); + prg + }) .collect(), keys: Vec::default(), extended: false, diff --git a/crates/ot/src/kos.rs b/crates/ot/src/kos.rs index fa7fb0b90..db25545c1 100644 --- a/crates/ot/src/kos.rs +++ b/crates/ot/src/kos.rs @@ -13,8 +13,8 @@ pub use receiver::Receiver; pub use sender::Sender; pub use mpz_ot_core::kos::{ - ReceiverConfig, ReceiverConfigBuilder, ReceiverConfigBuilderError, SenderConfig, - SenderConfigBuilder, SenderConfigBuilderError, msgs, + InstanceId, InstanceIds, ReceiverConfig, ReceiverConfigBuilder, ReceiverConfigBuilderError, + SenderConfig, SenderConfigBuilder, SenderConfigBuilderError, msgs, }; #[cfg(test)] @@ -31,8 +31,13 @@ mod tests { let mut rng = StdRng::seed_from_u64(0); let (base_sender, base_receiver) = ideal_ot(); let delta = Block::random(&mut rng); - let sender = Sender::new(SenderConfig::default(), delta, Block::ZERO, base_receiver); - let receiver = Receiver::new(ReceiverConfig::default(), Block::ZERO, base_sender); + let sender = Sender::new( + SenderConfig::default(), + delta, + InstanceId::SOLO, + base_receiver, + ); + let receiver = Receiver::new(ReceiverConfig::default(), InstanceId::SOLO, base_sender); test_rcot(sender, receiver, 128, 1).await; } diff --git a/crates/ot/src/kos/receiver.rs b/crates/ot/src/kos/receiver.rs index fb9e6e835..1ab5b3171 100644 --- a/crates/ot/src/kos/receiver.rs +++ b/crates/ot/src/kos/receiver.rs @@ -5,7 +5,10 @@ use serio::{SinkExt as _, stream::IoStreamExt}; use mpz_common::{Context, ContextError, Flush, future::MaybeDone}; use mpz_core::Block; use mpz_ot_core::{ - kos::{Receiver as Core, ReceiverConfig, ReceiverError as CoreError, receiver_state as state}, + kos::{ + InstanceId, Receiver as Core, ReceiverConfig, ReceiverError as CoreError, + receiver_state as state, + }, ot::OTSender, rcot::{RCOTReceiver, RCOTReceiverOutput}, }; @@ -40,9 +43,9 @@ impl Receiver { /// # Arguments /// /// * `config` - The Receiver's configuration. - /// * `instance_id` - Domain separator; must match the paired sender. + /// * `instance_id` - Domain separator. Must equal the paired sender's. /// * `base_ot` - Base OT. - pub fn new(config: ReceiverConfig, instance_id: Block, base_ot: BaseOT) -> Self { + pub fn new(config: ReceiverConfig, instance_id: InstanceId, base_ot: BaseOT) -> Self { Self { state: State::Initialized { base_ot, diff --git a/crates/ot/src/kos/sender.rs b/crates/ot/src/kos/sender.rs index 7c448a36c..ca3e66a6e 100644 --- a/crates/ot/src/kos/sender.rs +++ b/crates/ot/src/kos/sender.rs @@ -3,7 +3,9 @@ use itybity::IntoBits; use mpz_common::{Context, ContextError, Flush, future::MaybeDone}; use mpz_core::Block; use mpz_ot_core::{ - kos::{Sender as Core, SenderConfig, SenderError as CoreError, sender_state as state}, + kos::{ + InstanceId, Sender as Core, SenderConfig, SenderError as CoreError, sender_state as state, + }, ot::{OTReceiver, OTReceiverOutput}, rcot::{RCOTSender, RCOTSenderOutput}, }; @@ -40,10 +42,15 @@ impl Sender { /// /// * `config` - The Sender's configuration. /// * `delta` - Global COT correlation. - /// * `instance_id` - Domain separator; must match the paired receiver and - /// differ across instances that reuse `delta`. + /// * `instance_id` - Domain separator. Must equal the paired receiver's, + /// and differ from every other instance sharing `delta`. /// * `base_ot` - Base OT. - pub fn new(config: SenderConfig, delta: Block, instance_id: Block, base_ot: BaseOT) -> Self { + pub fn new( + config: SenderConfig, + delta: Block, + instance_id: InstanceId, + base_ot: BaseOT, + ) -> Self { Self { state: State::Initialized { base_ot,