Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix the saturated addition panic inside IPA #1286

Merged
merged 5 commits into from
Sep 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,11 @@ jobs:
- name: Run arithmetic bench
run: cargo bench --bench oneshot_arithmetic --no-default-features --features "enable-benches compact-gate"

- name: Run compact gate tests
- name: Run compact gate tests for HTTP stack
run: cargo test --no-default-features --features "cli web-app real-world-infra test-fixture compact-gate"

- name: Run in-memory compact gate tests
run: cargo test --features "compact-gate"
slow:
name: Slow tests
env:
Expand Down
1 change: 0 additions & 1 deletion ipa-core/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ track_steps!(
dp::step,
step,
},
test_fixture::step
);

fn main() {
Expand Down
3 changes: 2 additions & 1 deletion ipa-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ pub(crate) mod test_executor {
}
}

#[cfg(all(test, unit_test, not(feature = "shuttle")))]
#[cfg(all(test, not(feature = "shuttle")))]
pub(crate) mod test_executor {
use std::future::Future;

Expand All @@ -137,6 +137,7 @@ pub(crate) mod test_executor {
.block_on(f())
}

#[allow(dead_code)]
pub fn run<F, Fut, T>(f: F) -> T
where
F: Fn() -> Fut + Send + Sync + 'static,
Expand Down
2 changes: 1 addition & 1 deletion ipa-core/src/net/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -468,10 +468,10 @@ pub(crate) mod tests {
RequestHandler, RoleAssignment, Transport, MESSAGE_PAYLOAD_SIZE_BYTES,
},
net::test::TestServer,
protocol::step::TestExecutionStep,
query::ProtocolResult,
secret_sharing::replicated::semi_honest::AdditiveShare as Replicated,
sync::Arc,
test_fixture::step::TestExecutionStep,
};

#[tokio::test]
Expand Down
5 changes: 5 additions & 0 deletions ipa-core/src/protocol/basics/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,11 @@ impl<'a, B: ShardBinding> BooleanProtocols<UpgradedSemiHonestContext<'a, B, Bool
{
}

impl<'a, B: ShardBinding> BooleanProtocols<UpgradedSemiHonestContext<'a, B, Boolean>, 3>
for AdditiveShare<Boolean, 3>
{
}

impl<'a, B: ShardBinding> BooleanProtocols<DZKPUpgradedSemiHonestContext<'a, B>, 32>
for AdditiveShare<Boolean, 32>
{
Expand Down
10 changes: 7 additions & 3 deletions ipa-core/src/protocol/boolean/or.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
use std::iter::zip;

use ipa_step::StepNarrow;

use crate::{
error::Error,
ff::{boolean::Boolean, Field},
protocol::{basics::SecureMul, boolean::step::SixteenBitStep, context::Context, RecordId},
protocol::{basics::SecureMul, boolean::NBitStep, context::Context, Gate, RecordId},
secret_sharing::{
replicated::semi_honest::AdditiveShare, BitDecomposed, FieldSimd,
Linear as LinearSecretSharing,
Expand Down Expand Up @@ -34,25 +36,27 @@ pub async fn or<F: Field, C: Context, S: LinearSecretSharing<F> + SecureMul<C>>(
//
// Supplying an iterator saves constructing a complete copy of the argument
// in memory when it is a uniform constant.
pub async fn bool_or<'a, C, BI, const N: usize>(
pub async fn bool_or<'a, C, S, BI, const N: usize>(
ctx: C,
record_id: RecordId,
a: &BitDecomposed<AdditiveShare<Boolean, N>>,
b: BI,
) -> Result<BitDecomposed<AdditiveShare<Boolean, N>>, Error>
where
C: Context,
S: NBitStep,
BI: IntoIterator,
<BI as IntoIterator>::IntoIter: ExactSizeIterator<Item = &'a AdditiveShare<Boolean, N>> + Send,
Boolean: FieldSimd<N>,
AdditiveShare<Boolean, N>: SecureMul<C>,
Gate: StepNarrow<S>,
{
let b = b.into_iter();
assert_eq!(a.len(), b.len());

BitDecomposed::try_from(
ctx.parallel_join(zip(a.iter(), b).enumerate().map(|(i, (a, b))| {
let ctx = ctx.narrow(&SixteenBitStep::from(i));
let ctx = ctx.narrow(&S::from(i));
async move {
let ab = a.multiply(b, ctx, record_id).await?;
Ok::<_, Error>(-ab + a + b)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ where
.await?;

// if carry==1 then {all ones} else {result}
bool_or(
bool_or::<_, S, _, N>(
ctx.narrow::<Step>(&Step::Select),
record_id,
&result,
Expand Down
6 changes: 3 additions & 3 deletions ipa-core/src/protocol/ipa_prf/boolean_ops/step.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
use ipa_step_derive::CompactStep;

/// FIXME: This step is not generic enough to be used in the `saturated_addition` protocol.
/// It constrains the input to be at most 2 bytes and it will panic in runtime if it is greater
/// It constrains the input to be at most 4 bytes and it will panic in runtime if it is greater
/// than that. The issue is that compact gate requires concrete type to be put as child.
/// If we ever see it being an issue, we should make a few implementations of this similar to what
/// we've done for bit steps
#[derive(CompactStep)]
pub(crate) enum SaturatedAdditionStep {
#[step(child = crate::protocol::boolean::step::SixteenBitStep)]
#[step(child = crate::protocol::boolean::step::ThirtyTwoBitStep)]
Add,
#[step(child = crate::protocol::boolean::step::SixteenBitStep)]
#[step(child = crate::protocol::boolean::step::ThirtyTwoBitStep)]
Select,
}

Expand Down
106 changes: 106 additions & 0 deletions ipa-core/src/protocol/ipa_prf/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -721,3 +721,109 @@ pub mod tests {
});
}
}

#[cfg(all(test, all(feature = "compact-gate", feature = "in-memory-infra")))]
mod compact_gate_tests {

use ipa_step::StepNarrow;

use crate::{
ff::{
boolean_array::{BA20, BA5, BA8},
U128Conversions,
},
helpers::query::DpMechanism,
protocol::{
ipa_prf::{oprf_ipa, oprf_padding::PaddingParameters},
step::{ProtocolGate, ProtocolStep},
},
test_executor::run,
test_fixture::{ipa::TestRawDataRecord, Reconstruct, Runner, TestWorld, TestWorldConfig},
};

#[test]
fn saturated_agg() {
const EXPECTED: &[u128] = &[0, 255, 255, 0, 0, 0, 0, 0];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we're trying to test going from 2 bytes to 4, shouldn't we test some much bigger numbers?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the histogram value is 1 byte wide, so we definitely hit overflow with this test data which was our goal for this test. Any deviation from "standard" 8 bit trigger values requires transpose plumbing which seemed unnecessary here


run(|| async {
let world = TestWorld::new_with(TestWorldConfig {
initial_gate: Some(ProtocolGate::default().narrow(&ProtocolStep::IpaPrf)),
..Default::default()
});

let records: Vec<TestRawDataRecord> = vec![
TestRawDataRecord {
timestamp: 0,
user_id: 12345,
is_trigger_report: false,
breakdown_key: 1,
trigger_value: 0,
},
TestRawDataRecord {
timestamp: 5,
user_id: 12345,
is_trigger_report: false,
breakdown_key: 2,
trigger_value: 0,
},
TestRawDataRecord {
timestamp: 10,
user_id: 12345,
is_trigger_report: true,
breakdown_key: 0,
trigger_value: 255,
},
TestRawDataRecord {
timestamp: 20,
user_id: 12345,
is_trigger_report: true,
breakdown_key: 0,
eriktaubeneck marked this conversation as resolved.
Show resolved Hide resolved
trigger_value: 255,
},
TestRawDataRecord {
timestamp: 30,
user_id: 12345,
is_trigger_report: true,
breakdown_key: 0,
trigger_value: 255,
},
TestRawDataRecord {
timestamp: 0,
user_id: 68362,
is_trigger_report: false,
breakdown_key: 1,
trigger_value: 0,
},
TestRawDataRecord {
timestamp: 20,
user_id: 68362,
is_trigger_report: true,
breakdown_key: 1,
trigger_value: 255,
},
];
let dp_params = DpMechanism::NoDp;
let padding_params = PaddingParameters::relaxed();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doesn't this happen by default when testing?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure what you mean by that. We have to supply this padding_params parameter for oprf_ipa, so we need to pick an explicit value (Rust does not support default values for function parameters)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

got it. I was thinking this was gated by feature flags, but I suppose that just means this is unavailable without that flag.


let mut result: Vec<_> = world
.semi_honest(records.into_iter(), |ctx, input_rows| async move {
oprf_ipa::<_, BA5, BA8, BA8, BA20, 5, 32>(
ctx,
input_rows,
None,
dp_params,
padding_params,
)
.await
.unwrap()
})
.await
.reconstruct();
result.truncate(EXPECTED.len());
assert_eq!(
result.iter().map(|&v| v.as_u128()).collect::<Vec<_>>(),
EXPECTED,
);
});
}
}
4 changes: 2 additions & 2 deletions ipa-core/src/protocol/ipa_prf/prf_sharding/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -320,14 +320,14 @@ pub struct AttributionOutputs<BK, TV> {
pub type SecretSharedAttributionOutputs<BK, TV> =
AttributionOutputs<Replicated<BK>, Replicated<TV>>;

#[cfg(all(test, any(unit_test, feature = "shuttle")))]
#[cfg(test)]
#[derive(Debug, Clone, Ord, PartialEq, PartialOrd, Eq)]
pub struct AttributionOutputsTestInput<BK: BooleanArray, TV: BooleanArray> {
pub bk: BK,
pub tv: TV,
}

#[cfg(all(test, any(unit_test, feature = "shuttle")))]
#[cfg(test)]
impl<BK, TV> crate::secret_sharing::IntoShares<(Replicated<BK>, Replicated<TV>)>
for AttributionOutputsTestInput<BK, TV>
where
Expand Down
15 changes: 12 additions & 3 deletions ipa-core/src/protocol/step.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ pub enum ProtocolStep {
IpaPrf,
Multiply,
PrimeFieldAddition,
#[cfg(any(test, feature = "test-fixture"))]
#[step(count = 10, child = crate::test_fixture::step::TestExecutionStep)]
Test(usize),
/// Steps used in unit tests are grouped under this one. Ideally it should be
/// gated behind test configuration, but it does not work with build.rs that
/// does not enable any features when creating protocol gate file
#[step(child = TestExecutionStep)]
Test,

/// This step includes all the steps that are currently not linked into a top-level protocol.
///
Expand Down Expand Up @@ -46,3 +48,10 @@ pub enum DeadCodeStep {
#[step(child = crate::protocol::ipa_prf::boolean_ops::step::MultiplicationStep)]
Multiplication,
}

/// Provides a unique per-iteration context in tests.
#[derive(CompactStep)]
pub enum TestExecutionStep {
#[step(count = 999)]
Iter(usize),
}
2 changes: 1 addition & 1 deletion ipa-core/src/secret_sharing/into_shares.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ where
}
}

#[cfg(all(test, unit_test))]
#[cfg(test)]
eriktaubeneck marked this conversation as resolved.
Show resolved Hide resolved
impl<U, T> IntoShares<Result<T, crate::error::Error>> for Result<U, crate::error::Error>
where
U: IntoShares<T>,
Expand Down
6 changes: 3 additions & 3 deletions ipa-core/src/test_fixture/circuit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ use crate::{
protocol::{
basics::SecureMul,
context::{Context, SemiHonestContext},
step::ProtocolStep,
step::{ProtocolStep, TestExecutionStep as Step},
Gate, RecordId,
},
rand::thread_rng,
secret_sharing::{replicated::semi_honest::AdditiveShare as Replicated, FieldSimd, IntoShares},
seq_join::seq_join,
test_fixture::{step::TestExecutionStep as Step, ReconstructArr, TestWorld, TestWorldConfig},
test_fixture::{ReconstructArr, TestWorld, TestWorldConfig},
utils::array::zip3,
};

Expand Down Expand Up @@ -82,7 +82,7 @@ pub async fn arithmetic<F, const N: usize>(
active,
..Default::default()
},
initial_gate: Some(Gate::default().narrow(&ProtocolStep::Test(0))),
initial_gate: Some(Gate::default().narrow(&ProtocolStep::Test)),
..Default::default()
};
let world = TestWorld::new_with(config);
Expand Down
1 change: 0 additions & 1 deletion ipa-core/src/test_fixture/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ pub mod hybrid;
pub mod ipa;
pub mod logging;
pub mod metrics;
pub(crate) mod step;
#[cfg(feature = "in-memory-infra")]
mod test_gate;

Expand Down
8 changes: 0 additions & 8 deletions ipa-core/src/test_fixture/step.rs

This file was deleted.

2 changes: 1 addition & 1 deletion ipa-core/src/test_fixture/test_gate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::sync::atomic::{AtomicUsize, Ordering};

use ipa_step::StepNarrow;

use crate::{protocol::Gate, test_fixture::step::TestExecutionStep};
use crate::protocol::{step::TestExecutionStep, Gate};

/// This manages the gate information for test runs. Most unit tests want to have multiple runs
/// using the same instance of [`TestWorld`], but they don't care about the name of that particular
Expand Down
3 changes: 2 additions & 1 deletion ipa-step/src/gate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
let short_name = t.rsplit_once("::").map_or_else(|| t.as_ref(), |(_a, b)| b);
let msg = format!("unexpected narrow for {gate_name}({{s}}) => {short_name}({{ss}})");
syntax.extend(quote! {
#[allow(clippy::too_many_lines)]
#[allow(clippy::too_many_lines, clippy::unreadable_literal)]
impl ::ipa_step::StepNarrow<#ty> for #ident {
fn narrow(&self, step: &#ty) -> Self {
match self.0 {
Expand Down Expand Up @@ -87,6 +87,7 @@
let gate_lookup_type = step_hasher.lookup_type();
let mut syntax = quote! {

#[allow(clippy::unreadable_literal)]

Check warning on line 90 in ipa-step/src/gate.rs

View check run for this annotation

Codecov / codecov/patch

ipa-step/src/gate.rs#L90

Added line #L90 was not covered by tests
static STR_LOOKUP: [&str; #step_count] = [#(#gate_names),*];
static GATE_LOOKUP: #gate_lookup_type = #step_hasher

Expand Down
1 change: 1 addition & 0 deletions ipa-step/src/hash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ impl ToTokens for HashingSteps {
};

struct #lookup_type {
#[allow(clippy::unreadable_literal)]
inner: [(u64, u32); #sz]
}

Expand Down
3 changes: 3 additions & 0 deletions scripts/coverage-ci
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,7 @@ done
cargo test --bench oneshot_ipa --no-default-features --features "enable-benches compact-gate" -- -n 62 -c 16
cargo test --bench criterion_arithmetic --no-default-features --features "enable-benches compact-gate"

# compact gate + in-memory-infra
cargo test --features "compact-gate"

cargo llvm-cov report "$@"