Refactored C++ validation functions, behavior for overflows
C++:
- refactored validation functions
- added clang-format config
- reverted behavior for overflows
Go:
- fixed rounding integers to granularity for Gaussian and Laplace noise
GitOrigin-RevId: 14f17d401a353975899ddbbbd33d36261ef02dde
Change-Id: I3d298922f4529b59a682600c8d48f48b7854d387
diff --git a/.clang-format b/.clang-format
new file mode 100644
index 0000000..e0cd94a
--- /dev/null
+++ b/.clang-format
@@ -0,0 +1,5 @@
+---
+Language: Cpp
+BasedOnStyle: Google
+SortIncludes: false
+...
diff --git a/README.md b/README.md
index a756574..fc87de2 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@
private aggregations. Privacy on Beam is implemented using these libraries.
* A [stochastic tester](cc/testing), used to help catch regressions that could
make the differential privacy property no longer hold.
-* A [differential privacy accounting library](python/accounting), used for
+* A [differential privacy accounting library](python/dp_accounting), used for
tracking privacy budget.
To get started on generating differentially private data, we recomend you follow
diff --git a/cc/algorithms/BUILD b/cc/algorithms/BUILD
index edac6f8..e2d20bc 100644
--- a/cc/algorithms/BUILD
+++ b/cc/algorithms/BUILD
@@ -313,6 +313,7 @@
":distributions",
":numerical-mechanisms-testing",
":util",
+ "//base/testing:status_matchers",
"@com_google_googletest//:gtest_main",
"@com_google_absl//absl/status",
],
diff --git a/cc/algorithms/algorithm.h b/cc/algorithms/algorithm.h
index eab5129..0949d29 100644
--- a/cc/algorithms/algorithm.h
+++ b/cc/algorithms/algorithm.h
@@ -55,7 +55,10 @@
// Epsilon is a standard parameter of differentially private
// algorithms. See "The Algorithmic Foundations of Differential Privacy" p17.
explicit Algorithm(double epsilon)
- : epsilon_(epsilon), privacy_budget_(kFullPrivacyBudget) {}
+ : epsilon_(epsilon), privacy_budget_(kFullPrivacyBudget) {
+ DCHECK_NE(epsilon, std::numeric_limits<double>::infinity());
+ DCHECK_GT(epsilon, 0.0);
+ }
virtual ~Algorithm() = default;
@@ -195,6 +198,26 @@
<< " is being used. Consider setting your own epsilon based "
"on privacy considerations.";
}
+ RETURN_IF_ERROR(ValidateIsFiniteAndPositive(epsilon_, "Epsilon"));
+
+ if (delta_.has_value()) {
+ RETURN_IF_ERROR(
+ ValidateIsInInclusiveInterval(delta_.value(), 0, 1, "Delta"));
+ } // TODO: Default delta_ to kDefaultDelta?
+
+ if (l0_sensitivity_.has_value()) {
+ RETURN_IF_ERROR(
+ ValidateIsPositive(l0_sensitivity_.value(),
+ "Maximum number of partitions that can be "
+ "contributed to (i.e., L0 sensitivity)"));
+ } // TODO: Default is set in UpdateAndBuildMechanism() below.
+
+ if (max_contributions_per_partition_.has_value()) {
+ RETURN_IF_ERROR(
+ ValidateIsPositive(max_contributions_per_partition_.value(),
+ "Maximum number of contributions per partition"));
+ } // TODO: Default is set in UpdateAndBuildMechanism() below.
+
return BuildAlgorithm();
}
@@ -263,6 +286,7 @@
}
// If not set, we are using 1 as default value for both, L0 and Linf, as
// fallback for existing clients.
+ // TODO: Refactor, consolidate, or remove defaults.
return clone->SetL0Sensitivity(l0_sensitivity_.value_or(1))
.SetLInfSensitivity(max_contributions_per_partition_.value_or(1))
.Build();
diff --git a/cc/algorithms/algorithm_test.cc b/cc/algorithms/algorithm_test.cc
index 27080df..127a06c 100644
--- a/cc/algorithms/algorithm_test.cc
+++ b/cc/algorithms/algorithm_test.cc
@@ -29,13 +29,31 @@
namespace {
using ::testing::DoubleNear;
+using ::testing::HasSubstr;
+using ::differential_privacy::base::testing::StatusIs;
const double kTestPrecision = 1e-5;
template <typename T>
class TestAlgorithm : public Algorithm<T> {
public:
+ class Builder : public AlgorithmBuilder<T, TestAlgorithm<T>, Builder> {
+ using AlgorithmBuilder =
+ differential_privacy::AlgorithmBuilder<T, TestAlgorithm<T>, Builder>;
+
+ public:
+ Builder() : AlgorithmBuilder() {}
+
+ private:
+ base::StatusOr<std::unique_ptr<TestAlgorithm<T>>> BuildAlgorithm()
+ override {
+ return absl::WrapUnique(
+ new TestAlgorithm(AlgorithmBuilder::GetEpsilon().value()));
+ }
+ };
+
TestAlgorithm() : Algorithm<T>(1.0) {}
+ TestAlgorithm(double epsilon) : Algorithm<T>(epsilon) {}
void AddEntry(const T& t) override {}
Summary Serialize() override { return Summary(); }
absl::Status Merge(const Summary& summary) override {
@@ -113,5 +131,90 @@
EXPECT_DEATH(alg.ConsumePrivacyBudget(0.6), "Requested budget.*");
}
+TEST(IncrementalAlgorithmDeathTest, InvalidEpsilon) {
+ EXPECT_DEATH(TestAlgorithm<double> alg(-1.0), "Check failed: epsilon > 0.0");
+ EXPECT_DEATH(
+ TestAlgorithm<double> alg(std::numeric_limits<double>::quiet_NaN()),
+ "Check failed: epsilon > 0.0");
+ EXPECT_DEATH(
+ TestAlgorithm<double> alg(std::numeric_limits<double>::infinity()),
+ "Check failed: epsilon != std::numeric_limits<double>::infinity.*");
+}
+
+TEST(IncrementalAlgorithmBuilderTest, InvalidEpsilonFailsBuild) {
+ TestAlgorithm<double>::Builder builder;
+
+ EXPECT_THAT(builder.SetEpsilon(-1).Build(),
+ StatusIs(absl::StatusCode::kInvalidArgument,
+ HasSubstr("Epsilon must be finite and positive")));
+
+ EXPECT_THAT(
+ builder.SetEpsilon(std::numeric_limits<double>::quiet_NaN()).Build(),
+ StatusIs(absl::StatusCode::kInvalidArgument,
+ HasSubstr("Epsilon must be a valid numeric value")));
+
+ EXPECT_THAT(
+ builder.SetEpsilon(std::numeric_limits<double>::infinity()).Build(),
+ StatusIs(absl::StatusCode::kInvalidArgument,
+ HasSubstr("Epsilon must be finite")));
+}
+
+TEST(IncrementalAlgorithmBuilderTest, InvalidDeltaFailsBuild) {
+ TestAlgorithm<double>::Builder builder;
+
+ EXPECT_THAT(
+ builder.SetDelta(-0.1).Build(),
+ StatusIs(absl::StatusCode::kInvalidArgument,
+ HasSubstr("Delta must be in the inclusive interval [0,1]")));
+
+ EXPECT_THAT(
+ builder.SetDelta(1.1).Build(),
+ StatusIs(absl::StatusCode::kInvalidArgument,
+ HasSubstr("Delta must be in the inclusive interval [0,1]")));
+
+ EXPECT_THAT(
+ builder.SetDelta(std::numeric_limits<double>::quiet_NaN()).Build(),
+ StatusIs(absl::StatusCode::kInvalidArgument,
+ HasSubstr("Delta must be a valid numeric value")));
+
+ EXPECT_THAT(
+ builder.SetDelta(std::numeric_limits<double>::infinity()).Build(),
+ StatusIs(absl::StatusCode::kInvalidArgument,
+ HasSubstr("Delta must be in the inclusive interval [0,1]")));
+}
+
+TEST(IncrementalAlgorithmBuilderTest, InvalidL0SensitivityFailsBuild) {
+ TestAlgorithm<double>::Builder builder;
+
+ EXPECT_THAT(
+ builder.SetMaxPartitionsContributed(-1).Build(),
+ StatusIs(
+ absl::StatusCode::kInvalidArgument,
+ HasSubstr("Maximum number of partitions that can be "
+ "contributed to (i.e., L0 sensitivity) must be positive")));
+
+ EXPECT_THAT(
+ builder.SetMaxPartitionsContributed(0).Build(),
+ StatusIs(
+ absl::StatusCode::kInvalidArgument,
+ HasSubstr("Maximum number of partitions that can be "
+ "contributed to (i.e., L0 sensitivity) must be positive")));
+}
+
+TEST(IncrementalAlgorithmBuilderTest,
+ InvalidMaxContributionsPerPartitionFailsBuild) {
+ TestAlgorithm<double>::Builder builder;
+
+ EXPECT_THAT(builder.SetMaxContributionsPerPartition(-1).Build(),
+ StatusIs(absl::StatusCode::kInvalidArgument,
+ HasSubstr("Maximum number of contributions per "
+ "partition must be positive")));
+
+ EXPECT_THAT(builder.SetMaxContributionsPerPartition(0).Build(),
+ StatusIs(absl::StatusCode::kInvalidArgument,
+ HasSubstr("Maximum number of contributions per "
+ "partition must be positive")));
+}
+
} // namespace
} // namespace differential_privacy
diff --git a/cc/algorithms/approx-bounds.h b/cc/algorithms/approx-bounds.h
index afa6fb2..7f95552 100644
--- a/cc/algorithms/approx-bounds.h
+++ b/cc/algorithms/approx-bounds.h
@@ -141,27 +141,20 @@
// Check the validity of the histogram parameters. num_bin and
// success_probability restrictions prevent undefined threshold
// calculation.
- if (num_bins_ < 1) {
- return absl::InvalidArgumentError("Must have one or more bins.");
- }
- if (scale_ <= 0) {
- return absl::InvalidArgumentError("Scale must be positive.");
- }
- if (base_ <= 1) {
- return absl::InvalidArgumentError("Base must be greater than 1.");
- }
+ RETURN_IF_ERROR(ValidateIsPositive(num_bins_, "Number of bins"));
+ RETURN_IF_ERROR(ValidateIsFiniteAndPositive(scale_, "Scale"));
+ RETURN_IF_ERROR(ValidateIsFinite(base_, "Base"));
+ RETURN_IF_ERROR(ValidateIsGreaterThanOrEqualTo(base_, 1, "Base"));
+
// TODO: Handle case where scale * base^num_bins >
// std::numeric_limits<T>::max, even though the ApproxBounds constructor
// addresses this
if (has_k_) {
- if (k_ < 0) {
- return absl::InvalidArgumentError("k threshold must be nonnegative.");
- }
+ RETURN_IF_ERROR(ValidateIsFinite(k_, "k threshold"));
+ RETURN_IF_ERROR(ValidateIsNonNegative(k_, "k threshold"));
} else {
- if (success_probability_ <= 0 || success_probability_ >= 1) {
- return absl::InvalidArgumentError(
- "Success percentage must be between 0 and 1.");
- }
+ RETURN_IF_ERROR(ValidateIsInExclusiveInterval(
+ success_probability_, 0, 1, "Success probability"));
}
if (!has_k_) {
@@ -225,8 +218,8 @@
// Add bin count from summary to each bin.
for (int i = 0; i < pos_bins_.size(); ++i) {
- SafeAdd<int64_t>(pos_bins_[i], am_summary.pos_bin_count(i), &pos_bins_[i]);
- SafeAdd<int64_t>(neg_bins_[i], am_summary.neg_bin_count(i), &neg_bins_[i]);
+ pos_bins_[i] += am_summary.pos_bin_count(i);
+ neg_bins_[i] += am_summary.neg_bin_count(i);
}
return absl::OkStatus();
}
@@ -336,33 +329,29 @@
// to 0 to upper, and also from 0 to lower in the negative vectors.
if (lower < 0) {
for (int i = 0; i <= lower_msb; ++i) {
- SafeAdd<T2>(value, neg_partials[i], &value);
+ value += neg_partials[i];
}
}
if (upper > 0) {
for (int i = 0; i <= upper_msb; ++i) {
- SafeAdd<T2>(value, pos_partials[i], &value);
+ value += pos_partials[i];
}
}
} else if (upper < 0) {
// If lower and upper are negative, each value is clamped so that they
// contributed at most upper. Anything less they contributed is stored
// in partial values between lower and upper, which we add.
- T2 bound_product;
- SafeMultiply<T2>(value_transform(upper), count, &bound_product);
- SafeAdd<T2>(value, bound_product, &value);
+ value += count * value_transform(upper);
for (int i = upper_msb + 1; i <= lower_msb; ++i) {
- SafeAdd<T2>(value, neg_partials[i], &value);
+ value += neg_partials[i];
}
} else { // 0 < lower <= upper
// If lower and upper are both positive, each value is clamped to it
// contributed at least lower. Anything more contributed is stored
// between lower and upper in positive vectors, which we add.
- T2 bound_product;
- SafeMultiply<T2>(value_transform(lower), count, &bound_product);
- SafeAdd<T2>(value, bound_product, &value);
+ value += count * value_transform(lower);
for (int i = lower_msb + 1; i <= upper_msb; ++i) {
- SafeAdd<T2>(value, pos_partials[i], &value);
+ value += pos_partials[i];
}
}
return value;
@@ -416,9 +405,8 @@
// the threshold, populate the output with an error status.
base::StatusOr<Output> GenerateResult(double privacy_budget,
double noise_interval_level) override {
- DCHECK_GT(privacy_budget, 0.0)
- << "Privacy budget should be greater than zero.";
- if (privacy_budget == 0.0) return Output();
+ RETURN_IF_ERROR(ValidateIsPositive(privacy_budget, "Privacy budget",
+ absl::StatusCode::kFailedPrecondition));
// If k was not user set, scale it by the privacy_budget to ensure the
// correct probability of success.
@@ -508,9 +496,9 @@
// that MostSignificantBit returns 0 for 0.
int index = MostSignificantBit(input);
if (input >= 0) {
- SafeAdd<int64_t>(pos_bins_[index], num_of_entries, &pos_bins_[index]);
+ pos_bins_[index] += num_of_entries;
} else { // value < 0
- SafeAdd<int64_t>(neg_bins_[index], num_of_entries, &neg_bins_[index]);
+ neg_bins_[index] += num_of_entries;
}
}
@@ -535,13 +523,11 @@
partial = make_partial(NegRightBinBoundary(i), NegLeftBinBoundary(i));
}
- T2 multiplied_partial;
if (i < msb) {
// For indices below the msb, add the maximum contribution
// (num_of_entries times) to the partial.
- SafeMultiply<T2>(partial, num_of_entries, &multiplied_partial);
- SafeAdd<T2>((*partials)[i], multiplied_partial, &(*partials)[i]);
+ (*partials)[i] += partial * num_of_entries;
} else {
// For i = msb, add the remaining contribution (num_of_entries times),
// but not more than the maximum contribution to the partial for this
@@ -554,11 +540,9 @@
remainder = make_partial(value, NegLeftBinBoundary(i));
}
if (std::abs(partial) < std::abs(remainder)) {
- SafeMultiply<T2>(partial, num_of_entries, &multiplied_partial);
- SafeAdd<T2>((*partials)[msb], multiplied_partial, &(*partials)[msb]);
+ (*partials)[msb] += partial * num_of_entries;
} else {
- SafeMultiply<T2>(remainder, num_of_entries, &multiplied_partial);
- SafeAdd<T2>((*partials)[msb], multiplied_partial, &(*partials)[msb]);
+ (*partials)[msb] += remainder * num_of_entries;
}
}
}
diff --git a/cc/algorithms/approx-bounds_test.cc b/cc/algorithms/approx-bounds_test.cc
index eef5b5b..973a219 100644
--- a/cc/algorithms/approx-bounds_test.cc
+++ b/cc/algorithms/approx-bounds_test.cc
@@ -16,8 +16,6 @@
#include "algorithms/approx-bounds.h"
-#include <limits>
-
#include "base/testing/proto_matchers.h"
#include "base/testing/status_matchers.h"
#include "gmock/gmock.h"
@@ -118,12 +116,30 @@
.SetLaplaceMechanism(absl::make_unique<ZeroNoiseMechanism::Builder>())
.Build();
ASSERT_OK(bounds);
- EXPECT_THAT(
- (*bounds)->PartialResult(),
- StatusIs(
- absl::StatusCode::kFailedPrecondition,
- HasSubstr(
- "run over a larger dataset or decrease success_probability")));
+ EXPECT_THAT((*bounds)->PartialResult(),
+ StatusIs(absl::StatusCode::kFailedPrecondition,
+ HasSubstr("run over a larger dataset or decrease "
+ "success_probability")));
+}
+
+TEST(ApproxBoundsTest, InsufficientPrivacyBudgetTest) {
+ std::vector<int64_t> a = {0, -5, -5, INT_MIN, -7, 7, 7, 3, -6, 6, 5, 1};
+
+ // Make ApproxBounds.
+ base::StatusOr<std::unique_ptr<ApproxBounds<int64_t>>> bounds =
+ ApproxBounds<int64_t>::Builder()
+ .SetNumBins(4)
+ .SetBase(2)
+ .SetThreshold(3)
+ .SetLaplaceMechanism(absl::make_unique<ZeroNoiseMechanism::Builder>())
+ .Build();
+ ASSERT_OK(bounds);
+ (*bounds)->AddEntries(a.begin(), a.end());
+ base::StatusOr<Output> result = (*bounds)->PartialResult();
+ ASSERT_OK(result);
+ EXPECT_THAT((*bounds)->PartialResult(),
+ StatusIs(absl::StatusCode::kFailedPrecondition,
+ HasSubstr("Privacy budget must be positive")));
}
TEST(ApproxBoundsTest, SmallScale) {
@@ -185,7 +201,7 @@
.SetSuccessProbability(.95)
.Build(),
StatusIs(absl::StatusCode::kInvalidArgument,
- HasSubstr("Must have one or more bins")));
+ HasSubstr("Number of bins must be positive")));
EXPECT_THAT(typename ApproxBounds<TypeParam>::Builder()
.SetNumBins(2)
.SetScale(0)
@@ -193,7 +209,23 @@
.SetSuccessProbability(.95)
.Build(),
StatusIs(absl::StatusCode::kInvalidArgument,
- HasSubstr("Scale must be positive")));
+ HasSubstr("Scale must be finite and positive")));
+ EXPECT_THAT(typename ApproxBounds<TypeParam>::Builder()
+ .SetNumBins(2)
+ .SetScale(std::numeric_limits<double>::infinity())
+ .SetBase(2)
+ .SetSuccessProbability(.95)
+ .Build(),
+ StatusIs(absl::StatusCode::kInvalidArgument,
+ HasSubstr("Scale must be finite and positive")));
+ EXPECT_THAT(typename ApproxBounds<TypeParam>::Builder()
+ .SetNumBins(2)
+ .SetScale(std::numeric_limits<double>::quiet_NaN())
+ .SetBase(2)
+ .SetSuccessProbability(.95)
+ .Build(),
+ StatusIs(absl::StatusCode::kInvalidArgument,
+ HasSubstr("Scale must be a valid numeric value")));
EXPECT_THAT(typename ApproxBounds<TypeParam>::Builder()
.SetNumBins(2)
.SetScale(1)
@@ -201,7 +233,23 @@
.SetSuccessProbability(.95)
.Build(),
StatusIs(absl::StatusCode::kInvalidArgument,
- HasSubstr("Base must be greater than 1")));
+ HasSubstr("Base must be greater than or equal to 1")));
+ EXPECT_THAT(typename ApproxBounds<TypeParam>::Builder()
+ .SetNumBins(2)
+ .SetScale(1)
+ .SetBase(std::numeric_limits<double>::infinity())
+ .SetSuccessProbability(.95)
+ .Build(),
+ StatusIs(absl::StatusCode::kInvalidArgument,
+ HasSubstr("Base must be finite")));
+ EXPECT_THAT(typename ApproxBounds<TypeParam>::Builder()
+ .SetNumBins(2)
+ .SetScale(1)
+ .SetBase(std::numeric_limits<double>::quiet_NaN())
+ .SetSuccessProbability(.95)
+ .Build(),
+ StatusIs(absl::StatusCode::kInvalidArgument,
+ HasSubstr("Base must be a valid numeric value")));
EXPECT_THAT(
typename ApproxBounds<TypeParam>::Builder()
.SetNumBins(2)
@@ -209,8 +257,41 @@
.SetBase(2)
.SetSuccessProbability(1)
.Build(),
+ StatusIs(
+ absl::StatusCode::kInvalidArgument,
+ HasSubstr(
+ "Success probability must be in the exclusive interval (0,1)")));
+ EXPECT_THAT(
+ typename ApproxBounds<TypeParam>::Builder()
+ .SetNumBins(2)
+ .SetScale(1)
+ .SetBase(2)
+ .SetSuccessProbability(0)
+ .Build(),
+ StatusIs(
+ absl::StatusCode::kInvalidArgument,
+ HasSubstr(
+ "Success probability must be in the exclusive interval (0,1)")));
+ EXPECT_THAT(
+ typename ApproxBounds<TypeParam>::Builder()
+ .SetNumBins(2)
+ .SetScale(1)
+ .SetBase(2)
+ .SetSuccessProbability(std::numeric_limits<double>::infinity())
+ .Build(),
+ StatusIs(
+ absl::StatusCode::kInvalidArgument,
+ HasSubstr(
+ "Success probability must be in the exclusive interval (0,1)")));
+ EXPECT_THAT(
+ typename ApproxBounds<TypeParam>::Builder()
+ .SetNumBins(2)
+ .SetScale(1)
+ .SetBase(2)
+ .SetSuccessProbability(std::numeric_limits<double>::quiet_NaN())
+ .Build(),
StatusIs(absl::StatusCode::kInvalidArgument,
- HasSubstr("Success percentage must be between 0 and 1")));
+ HasSubstr("Success probability must be a valid numeric value")));
EXPECT_THAT(typename ApproxBounds<TypeParam>::Builder()
.SetNumBins(2)
.SetScale(1)
@@ -218,7 +299,23 @@
.SetThreshold(-1)
.Build(),
StatusIs(absl::StatusCode::kInvalidArgument,
- HasSubstr("k threshold must be nonnegative")));
+ HasSubstr("k threshold must be non-negative")));
+ EXPECT_THAT(typename ApproxBounds<TypeParam>::Builder()
+ .SetNumBins(2)
+ .SetScale(1)
+ .SetBase(2)
+ .SetThreshold(std::numeric_limits<double>::infinity())
+ .Build(),
+ StatusIs(absl::StatusCode::kInvalidArgument,
+ HasSubstr("k threshold must be finite")));
+ EXPECT_THAT(typename ApproxBounds<TypeParam>::Builder()
+ .SetNumBins(2)
+ .SetScale(1)
+ .SetBase(2)
+ .SetThreshold(std::numeric_limits<double>::quiet_NaN())
+ .Build(),
+ StatusIs(absl::StatusCode::kInvalidArgument,
+ HasSubstr("k threshold must be a valid numeric value")));
}
TEST(ApproxBoundsTest, DefaultIntTest) {
@@ -292,71 +389,89 @@
TYPED_TEST(ApproxBoundsTest, SerializeAndMergeOverflowPosBinsTest) {
typename ApproxBounds<int64_t>::Builder builder;
- // Serialize bounds with only data from a.
- std::unique_ptr<ApproxBounds<int64_t>> bounds =
+ base::StatusOr<std::unique_ptr<ApproxBounds<int64_t>>> bounds =
builder.SetNumBins(3)
.SetBase(10)
.SetScale(1)
.SetThreshold(2)
.SetLaplaceMechanism(absl::make_unique<ZeroNoiseMechanism::Builder>())
- .Build()
- .ValueOrDie();
+ .Build();
+ ASSERT_OK(bounds);
ApproxBoundsTestPeer::AddMultipleEntries<int64_t>(
- 1, std::numeric_limits<int64_t>::max(), bounds.get());
- Summary summary = bounds->Serialize();
- bounds->AddEntry(1);
- bounds->AddEntry(1);
- bounds->AddEntry(1);
+ 1, std::numeric_limits<int64_t>::max(), (*bounds).get());
+ Summary summary = (*bounds)->Serialize();
+ (*bounds)->AddEntry(1);
+ (*bounds)->AddEntry(1);
+ (*bounds)->AddEntry(1);
// Create bounds2 with part of its data from merge.
- std::unique_ptr<ApproxBounds<int64_t>> bounds2 = builder.Build().ValueOrDie();
- EXPECT_OK(bounds2->Merge(summary));
- bounds2->AddEntry(1);
- bounds2->AddEntry(1);
- bounds2->AddEntry(1);
+ base::StatusOr<std::unique_ptr<ApproxBounds<int64_t>>> bounds2 =
+ builder.Build();
+ ASSERT_OK(bounds2);
+ EXPECT_OK((*bounds2)->Merge(summary));
+ (*bounds2)->AddEntry(1);
+ (*bounds2)->AddEntry(1);
+ (*bounds2)->AddEntry(1);
- // Check that results are the same.
- auto result = bounds->PartialResult().ValueOrDie();
- auto result2 = bounds2->PartialResult().ValueOrDie();
- EXPECT_EQ(result.elements(0).value().int_value(),
- result2.elements(0).value().int_value());
- EXPECT_EQ(result.elements(1).value().int_value(),
- result2.elements(1).value().int_value());
+ // The bin counts should have overflowed and be smaller than the threshold.
+ EXPECT_THAT((*bounds2)->PartialResult().status(),
+ StatusIs(absl::StatusCode::kFailedPrecondition,
+ HasSubstr("Bin count threshold was too large to find "
+ "approximate bounds.")));
+
+ // Ensure a pre-merge overflow is passed on during a serialize & merge
+ summary = (*bounds2)->Serialize();
+ bounds2 = builder.Build();
+ EXPECT_OK((*bounds2)->Merge(summary));
+ // The bin counts should have overflowed and be smaller than the threshold.
+ EXPECT_THAT((*bounds2)->PartialResult().status(),
+ StatusIs(absl::StatusCode::kFailedPrecondition,
+ HasSubstr("Bin count threshold was too large to find "
+ "approximate bounds.")));
}
TYPED_TEST(ApproxBoundsTest, SerializeAndMergeOverflowNegBinsTest) {
typename ApproxBounds<int64_t>::Builder builder;
- // Serialize bounds with only data from a.
- std::unique_ptr<ApproxBounds<int64_t>> bounds =
+ base::StatusOr<std::unique_ptr<ApproxBounds<int64_t>>> bounds =
builder.SetNumBins(3)
.SetBase(10)
.SetScale(1)
.SetThreshold(2)
.SetLaplaceMechanism(absl::make_unique<ZeroNoiseMechanism::Builder>())
- .Build()
- .ValueOrDie();
+ .Build();
+ ASSERT_OK(bounds);
ApproxBoundsTestPeer::AddMultipleEntries<int64_t>(
- -1, std::numeric_limits<int64_t>::max(), bounds.get());
- Summary summary = bounds->Serialize();
- bounds->AddEntry(-1);
- bounds->AddEntry(-1);
- bounds->AddEntry(-1);
+ -1, std::numeric_limits<int64_t>::max(), (*bounds).get());
+ Summary summary = (*bounds)->Serialize();
+ (*bounds)->AddEntry(-1);
+ (*bounds)->AddEntry(-1);
+ (*bounds)->AddEntry(-1);
// Create bounds2 with part of its data from merge.
- std::unique_ptr<ApproxBounds<int64_t>> bounds2 = builder.Build().ValueOrDie();
- EXPECT_OK(bounds2->Merge(summary));
- bounds2->AddEntry(-1);
- bounds2->AddEntry(-1);
- bounds2->AddEntry(-1);
+ base::StatusOr<std::unique_ptr<ApproxBounds<int64_t>>> bounds2 =
+ builder.Build();
+ ASSERT_OK(bounds2);
+ EXPECT_OK((*bounds2)->Merge(summary));
+ (*bounds2)->AddEntry(-1);
+ (*bounds2)->AddEntry(-1);
+ (*bounds2)->AddEntry(-1);
- // Check that results are the same.
- auto result = bounds->PartialResult().ValueOrDie();
- auto result2 = bounds2->PartialResult().ValueOrDie();
- EXPECT_EQ(result.elements(0).value().int_value(),
- result2.elements(0).value().int_value());
- EXPECT_EQ(result.elements(1).value().int_value(),
- result2.elements(1).value().int_value());
+ // The bin counts should have overflowed and be smaller than the threshold.
+ EXPECT_THAT((*bounds2)->PartialResult().status(),
+ StatusIs(absl::StatusCode::kFailedPrecondition,
+ HasSubstr("Bin count threshold was too large to find "
+ "approximate bounds.")));
+
+ // Ensure a pre-merge overflow is passed on during a serialize & merge
+ summary = (*bounds2)->Serialize();
+ bounds2 = builder.Build();
+ EXPECT_OK((*bounds2)->Merge(summary));
+ // The bin counts should have overflowed and be smaller than the threshold.
+ EXPECT_THAT((*bounds2)->PartialResult().status(),
+ StatusIs(absl::StatusCode::kFailedPrecondition,
+ HasSubstr("Bin count threshold was too large to find "
+ "approximate bounds.")));
}
TEST(ApproxBoundsTest, DropNanEntries) {
@@ -380,51 +495,56 @@
}
TEST(ApproxBoundsTest, HandleOverflowPosBins) {
- std::unique_ptr<ApproxBounds<int64_t>> bounds =
+ base::StatusOr<std::unique_ptr<ApproxBounds<int64_t>>> bounds =
ApproxBounds<int64_t>::Builder()
.SetNumBins(2)
.SetBase(2)
.SetScale(1)
.SetThreshold(2)
.SetLaplaceMechanism(absl::make_unique<ZeroNoiseMechanism::Builder>())
- .Build()
- .ValueOrDie();
+ .Build();
+ ASSERT_OK(bounds);
// Add std::numeric_limits<int64_t>::max() + 3 entries to the same bin to try to
// cause an overflow.
ApproxBoundsTestPeer::AddMultipleEntries<int64_t>(
- 1, std::numeric_limits<int64_t>::max(), bounds.get());
- bounds->AddEntry(1);
- bounds->AddEntry(1);
- bounds->AddEntry(1);
- auto result = bounds->PartialResult();
- // An overflow would cause a negative bin count and all bins to be below
+ 1, std::numeric_limits<int64_t>::max(), (*bounds).get());
+ (*bounds)->AddEntry(1);
+ (*bounds)->AddEntry(1);
+ (*bounds)->AddEntry(1);
+ base::StatusOr<Output> result = (*bounds)->PartialResult();
+ // An overflow should cause a negative bin count and all bins to be below
// the threshold, resulting in an error when there are no bins to return.
// Thus, if there is no error, there was no overflow.
- EXPECT_OK(result.status());
+ EXPECT_THAT(result.status(),
+ StatusIs(absl::StatusCode::kFailedPrecondition,
+ HasSubstr("Bin count threshold was too large to find "
+ "approximate bounds.")));
}
TEST(ApproxBoundsTest, HandleOverflowNegBins) {
- std::unique_ptr<ApproxBounds<int64_t>> bounds =
+ base::StatusOr<std::unique_ptr<ApproxBounds<int64_t>>> bounds =
ApproxBounds<int64_t>::Builder()
.SetNumBins(2)
.SetBase(2)
.SetScale(1)
.SetThreshold(2)
.SetLaplaceMechanism(absl::make_unique<ZeroNoiseMechanism::Builder>())
- .Build()
- .ValueOrDie();
+ .Build();
// Add std::numeric_limits<int64_t>::max() + 3 entries to the same bin to try to
// cause an overflow.
ApproxBoundsTestPeer::AddMultipleEntries<int64_t>(
- -1, std::numeric_limits<int64_t>::max(), bounds.get());
- bounds->AddEntry(-1);
- bounds->AddEntry(-1);
- bounds->AddEntry(-1);
- auto result = bounds->PartialResult();
- // An overflow would cause a negative bin count and all bins to be below
+ -1, std::numeric_limits<int64_t>::max(), (*bounds).get());
+ (*bounds)->AddEntry(-1);
+ (*bounds)->AddEntry(-1);
+ (*bounds)->AddEntry(-1);
+ base::StatusOr<Output> result = (*bounds)->PartialResult();
+ // An overflow should cause a negative bin count and all bins to be below
// the threshold, resulting in an error when there are no bins to return.
// Thus, if there is no error, there was no overflow.
- EXPECT_OK(result.status());
+ EXPECT_THAT(result.status(),
+ StatusIs(absl::StatusCode::kFailedPrecondition,
+ HasSubstr("Bin count threshold was too large to find "
+ "approximate bounds.")));
}
TEST(ApproxBoundsTest, HandleInfinityEntries) {
@@ -664,10 +784,9 @@
std::vector<int64_t> sums(n_bins, 0);
ApproxBoundsTestPeer::AddMultipleEntriesToPartialSums<int64_t, int64_t>(
&sums, 6, n_entries, bounds.value().get());
- for (int i = 0; i < n_bins; ++i) {
- // If there is an overflow, at least one of the bins will be negative
- EXPECT_GE(sums[i], 0);
- }
+
+ // If there is an overflow, at least one of the bins will be negative
+ EXPECT_THAT(sums, testing::Contains(testing::Lt(0)));
}
TYPED_TEST(ApproxBoundsTest, ComputeSumFromPartials) {
@@ -708,33 +827,25 @@
.Build();
ASSERT_OK(bounds);
- std::vector<int64_t> neg_sum = {-2, int64lowest, -1, int64lowest};
+ std::vector<int64_t> neg_sum = {0, -1, -2, int64lowest};
std::vector<int64_t> pos_sum = {0, 0, 0, 0};
int64_t result = (*bounds)->template ComputeFromPartials<int64_t>(
pos_sum, neg_sum, value_transform, int64lowest, int64max, 2);
- EXPECT_EQ(result, int64lowest);
+ EXPECT_GT(result, 0); // The negative sums should overflow to positive
result = (*bounds)->template ComputeFromPartials<int64_t>(
pos_sum, neg_sum, value_transform, int64lowest, -1, 2);
- EXPECT_EQ(result, int64lowest);
-
- result = (*bounds)->template ComputeFromPartials<int64_t>(
- pos_sum, neg_sum, value_transform, int64lowest, -2, int64max);
- EXPECT_EQ(result, int64lowest);
+ EXPECT_GT(result, 0); // The negative sums should overflow to positive
neg_sum = {0, 0, 0, 0};
- pos_sum = {2, int64max, 1, int64max};
+ pos_sum = {0, 1, 2, int64max};
result = (*bounds)->template ComputeFromPartials<int64_t>(
pos_sum, neg_sum, value_transform, int64lowest, int64max, 2);
- EXPECT_EQ(result, int64max);
+ EXPECT_LT(result, 0); // The positive sums should overflow to negative
result = (*bounds)->template ComputeFromPartials<int64_t>(
pos_sum, neg_sum, value_transform, 1, int64max, 2);
- EXPECT_EQ(result, int64max);
-
- result = (*bounds)->template ComputeFromPartials<int64_t>(
- pos_sum, neg_sum, value_transform, 1, int64max, int64max);
- EXPECT_EQ(result, int64max);
+ EXPECT_LT(result, 0); // The positive sums should overflow to negative
}
TEST(ApproxBoundsText, ComputeSumFromPartialsAcrossOne) {
diff --git a/cc/algorithms/binary-search.h b/cc/algorithms/binary-search.h
index 0cb8500..8be2bcc 100644
--- a/cc/algorithms/binary-search.h
+++ b/cc/algorithms/binary-search.h
@@ -134,15 +134,18 @@
upper_(upper),
lower_(lower),
mechanism_(std::move(mechanism)),
- quantiles_(std::move(input_sketch)) {}
+ quantiles_(std::move(input_sketch)) {
+ // TODO: Replace with Builder class & parameter validation
+ DCHECK_GE(quantile, 0);
+ DCHECK_LE(quantile, 1);
+ }
void ResetState() override { quantiles_->Reset(); }
base::StatusOr<Output> GenerateResult(double privacy_budget,
double noise_interval_level) override {
- DCHECK_GT(privacy_budget, 0.0)
- << "Privacy budget should be greater than zero.";
- if (privacy_budget == 0.0) return Output();
+ RETURN_IF_ERROR(ValidateIsPositive(privacy_budget, "Privacy budget",
+ absl::StatusCode::kFailedPrecondition));
return BayesianSearch(privacy_budget, noise_interval_level);
}
@@ -244,8 +247,7 @@
}
}
- // Round the result instead of truncation, and ensure the result is within
- // the valid bounds of T (to prevent overflows or underflows).
+ // Round the result instead of truncation.
if (std::is_integral<T>::value) {
m = Clamp<double>(std::numeric_limits<T>::lowest(),
std::numeric_limits<T>::max(), std::round(m));
diff --git a/cc/algorithms/bounded-algorithm.h b/cc/algorithms/bounded-algorithm.h
index 63bef76..47e5439 100644
--- a/cc/algorithms/bounded-algorithm.h
+++ b/cc/algorithms/bounded-algorithm.h
@@ -103,18 +103,6 @@
.SetLaplaceMechanism(std::move(mech_builder))
.Build());
}
- // Check if bounds are finite when a floating point type is used and bounds
- // have been set manually.
- if (BoundsAreSet() && std::is_floating_point<T>::value) {
- if (!std::isfinite(static_cast<double>(lower_.value()))) {
- return absl::InvalidArgumentError(absl::StrCat(
- "Lower bound has to be finite but is ", lower_.value()));
- }
- if (!std::isfinite(static_cast<double>(upper_.value()))) {
- return absl::InvalidArgumentError(absl::StrCat(
- "Upper bound has to be finite but is ", upper_.value()));
- }
- }
return absl::OkStatus();
}
@@ -152,17 +140,23 @@
// lower and upper bounds, respectively.
std::unique_ptr<ApproxBounds<T>> approx_bounds_;
- absl::Status CheckBoundsOrder() {
- if (BoundsAreSet() && lower_.value() > upper_.value()) {
- return absl::InvalidArgumentError(
- "Lower bound cannot be greater than upper bound.");
- }
- return absl::OkStatus();
- }
-
// Common initialization and checks for building bounded algorithms.
base::StatusOr<std::unique_ptr<Algorithm>> BuildAlgorithm() final {
- RETURN_IF_ERROR(CheckBoundsOrder());
+ if (lower_.has_value() != upper_.has_value()) {
+ return absl::InvalidArgumentError(
+ "Lower and upper bounds must either both be set or both be unset.");
+ }
+
+ if (BoundsAreSet()) {
+ RETURN_IF_ERROR(ValidateIsFinite(lower_.value(), "Lower bound"));
+ RETURN_IF_ERROR(ValidateIsFinite(upper_.value(), "Upper bound"));
+
+ if (lower_.value() > upper_.value()) {
+ return absl::InvalidArgumentError(
+ "Lower bound cannot be greater than upper bound.");
+ }
+ }
+
return BuildBoundedAlgorithm();
}
};
diff --git a/cc/algorithms/bounded-algorithm_test.cc b/cc/algorithms/bounded-algorithm_test.cc
index 2fab8da..d99f633 100644
--- a/cc/algorithms/bounded-algorithm_test.cc
+++ b/cc/algorithms/bounded-algorithm_test.cc
@@ -26,6 +26,9 @@
namespace differential_privacy {
namespace {
+using ::testing::HasSubstr;
+using ::differential_privacy::base::testing::StatusIs;
+
template <typename T>
class BoundedAlgorithmTest : public testing::Test {};
@@ -104,5 +107,54 @@
EXPECT_TRUE(builder.GetApproxBounds());
}
+TEST(BoundedAlgorithmTest, InvalidParameters) {
+ typename BoundedAlgorithm<double>::Builder builder;
+
+ builder.SetLower(-std::numeric_limits<double>::infinity());
+ builder.SetUpper(0.5);
+ EXPECT_THAT(builder.Build(),
+ StatusIs(absl::StatusCode::kInvalidArgument,
+ HasSubstr("Lower bound must be finite")));
+
+ builder.SetLower(-0.5);
+ builder.SetUpper(std::numeric_limits<double>::infinity());
+ EXPECT_THAT(builder.Build(),
+ StatusIs(absl::StatusCode::kInvalidArgument,
+ HasSubstr("Upper bound must be finite")));
+
+ builder.SetLower(0.5);
+ builder.SetUpper(-0.5);
+ EXPECT_THAT(
+ builder.Build(),
+ StatusIs(absl::StatusCode::kInvalidArgument,
+ HasSubstr("Lower bound cannot be greater than upper bound.")));
+
+ builder.ClearBounds();
+ builder.SetLower(-0.5);
+ EXPECT_THAT(builder.Build(),
+ StatusIs(absl::StatusCode::kInvalidArgument,
+ HasSubstr("Lower and upper bounds must either both be "
+ "set or both be unset.")));
+
+ builder.ClearBounds();
+ builder.SetUpper(-0.5);
+ EXPECT_THAT(builder.Build(),
+ StatusIs(absl::StatusCode::kInvalidArgument,
+ HasSubstr("Lower and upper bounds must either both be "
+ "set or both be unset.")));
+
+ builder.SetLower(std::numeric_limits<double>::quiet_NaN());
+ builder.SetUpper(0.5);
+ EXPECT_THAT(builder.Build(),
+ StatusIs(absl::StatusCode::kInvalidArgument,
+ HasSubstr("Lower bound must be a valid numeric value")));
+
+ builder.SetLower(-0.5);
+ builder.SetUpper(std::numeric_limits<double>::quiet_NaN());
+ EXPECT_THAT(builder.Build(),
+ StatusIs(absl::StatusCode::kInvalidArgument,
+ HasSubstr("Upper bound must be a valid numeric value")));
+}
+
} // namespace
} // namespace differential_privacy
diff --git a/cc/algorithms/bounded-mean.h b/cc/algorithms/bounded-mean.h
index daa55a6..f660755 100644
--- a/cc/algorithms/bounded-mean.h
+++ b/cc/algorithms/bounded-mean.h
@@ -76,9 +76,8 @@
override {
// We have to check epsilon now, otherwise the split during ApproxBounds
// construction might make the error message confusing.
- RETURN_IF_ERROR(
- GetValueIfSetAndPositive(AlgorithmBuilder::GetEpsilon(), "Epsilon")
- .status());
+ RETURN_IF_ERROR(ValidateIsFiniteAndPositive(
+ AlgorithmBuilder::GetEpsilon(), "Epsilon"));
// Ensure that either bounds are manually set or ApproxBounds is made.
RETURN_IF_ERROR(BoundedBuilder::BoundsSetup());
@@ -151,10 +150,6 @@
return summary;
}
- // Note that the partial sums pos_sum_[i] and neg_sum_[i] will not surpass T's
- // numeric limits (see SafeAdd implementation), but the raw counts will still
- // be added together. Thus, the resulting mean will be incorrect,
- // but warning the user of this would unfortunately violate DP.
absl::Status Merge(const Summary& summary) override {
if (!summary.has_data()) {
return absl::InternalError(
@@ -166,17 +161,17 @@
if (!summary.data().UnpackTo(&bm_summary)) {
return absl::InternalError("Bounded mean summary unable to be unpacked.");
}
- SafeAdd<uint64_t>(raw_count_, bm_summary.count(), &raw_count_);
+ raw_count_ += bm_summary.count();
if (pos_sum_.size() != bm_summary.pos_sum_size() ||
neg_sum_.size() != bm_summary.neg_sum_size()) {
return absl::InternalError(
"Merged BoundedMeans must have equal number of partial sums.");
}
for (int i = 0; i < pos_sum_.size(); ++i) {
- SafeAdd(pos_sum_[i], GetValue<T>(bm_summary.pos_sum(i)), &pos_sum_[i]);
+ pos_sum_[i] += GetValue<T>(bm_summary.pos_sum(i));
}
for (int i = 0; i < neg_sum_.size(); ++i) {
- SafeAdd(neg_sum_[i], GetValue<T>(bm_summary.neg_sum(i)), &neg_sum_[i]);
+ neg_sum_[i] += GetValue<T>(bm_summary.neg_sum(i));
}
if (approx_bounds_) {
Summary approx_bounds_summary;
@@ -255,9 +250,9 @@
base::StatusOr<Output> GenerateResult(double privacy_budget,
double noise_interval_level) override {
- DCHECK_GT(privacy_budget, 0.0)
- << "Privacy budget should be greater than zero.";
- if (privacy_budget == 0.0) return Output();
+ RETURN_IF_ERROR(ValidateIsPositive(privacy_budget, "Privacy budget",
+ absl::StatusCode::kFailedPrecondition));
+
double sum = 0;
double remaining_budget = privacy_budget;
Output output;
@@ -321,10 +316,6 @@
}
private:
- // Note that for manual bounds, pos_sum_[0] will not surpass T's numeric
- // limit (see SafeAdd implementation), but will continue to increment
- // raw_count_ for every call to AddEntry(). Thus, the resulting mean will be
- // incorrect, but warning the user of this would unfortunately violate DP.
void AddMultipleEntries(const T& input, uint64_t num_of_entries) {
// REF:
// https://stackoverflow.com/questions/61646166/how-to-resolve-fpclassify-ambiguous-call-to-overloaded-function
@@ -332,15 +323,10 @@
return;
}
- SafeAdd<uint64_t>(raw_count_, num_of_entries, &raw_count_);
+ raw_count_ += num_of_entries;
if (!approx_bounds_) {
- T total_input;
- SafeMultiply<T>(Clamp<T>(lower_, upper_, input),
- Clamp<T>(std::numeric_limits<T>::lowest(),
- std::numeric_limits<T>::max(), num_of_entries),
- &total_input);
- SafeAdd(pos_sum_[0], total_input, &pos_sum_[0]);
+ pos_sum_[0] += Clamp<T>(lower_, upper_, input) * num_of_entries;
} else {
approx_bounds_->AddMultipleEntries(input, num_of_entries);
diff --git a/cc/algorithms/bounded-mean_test.cc b/cc/algorithms/bounded-mean_test.cc
index 701a533..bcdf57f 100644
--- a/cc/algorithms/bounded-mean_test.cc
+++ b/cc/algorithms/bounded-mean_test.cc
@@ -122,6 +122,47 @@
EXPECT_DOUBLE_EQ(GetValue<double>(*result), 11.0 / 3.0);
}
+TEST(BoundedMeanTest, InvalidParametersTest) {
+ EXPECT_THAT(BoundedMean<double>::Builder()
+ .SetEpsilon(std::numeric_limits<double>::infinity())
+ .Build(),
+ StatusIs(absl::StatusCode::kInvalidArgument,
+ HasSubstr("Epsilon must be finite")));
+
+ EXPECT_THAT(BoundedMean<double>::Builder()
+ .SetEpsilon(std::numeric_limits<double>::quiet_NaN())
+ .Build(),
+ StatusIs(absl::StatusCode::kInvalidArgument,
+ HasSubstr("Epsilon must be a valid numeric value")));
+
+ EXPECT_THAT(BoundedMean<double>::Builder().SetEpsilon(-0.5).Build(),
+ StatusIs(absl::StatusCode::kInvalidArgument,
+ HasSubstr("Epsilon must be finite and positive")));
+
+ EXPECT_THAT(BoundedMean<double>::Builder().SetEpsilon(0).Build(),
+ StatusIs(absl::StatusCode::kInvalidArgument,
+ HasSubstr("Epsilon must be finite and positive")));
+}
+
+TYPED_TEST(BoundedMeanTest, InsufficientPrivacyBudgetTest) {
+ std::vector<TypeParam> a = {2, 4, 6, 8};
+
+ auto mean =
+ typename BoundedMean<TypeParam>::Builder()
+ .SetLaplaceMechanism(absl::make_unique<ZeroNoiseMechanism::Builder>())
+ .SetEpsilon(1.0)
+ .SetLower(1)
+ .SetUpper(9)
+ .Build();
+ ASSERT_OK(mean);
+ (*mean)->AddEntries(a.begin(), a.end());
+
+ ASSERT_OK((*mean)->PartialResult());
+ EXPECT_THAT((*mean)->PartialResult(),
+ StatusIs(absl::StatusCode::kFailedPrecondition,
+ HasSubstr("Privacy budget must be positive")));
+}
+
// This test verifies that BoundedMean never returns a value outside of the
// bounds, even if BoundedSum/Count would be outside the bounds.
TYPED_TEST(BoundedMeanTest, LowClampTest) {
@@ -222,166 +263,190 @@
TEST(BoundedMeanTest, OverflowRawCountTest) {
typename BoundedMean<double>::Builder builder;
- std::unique_ptr<BoundedMean<double>> bm =
+ base::StatusOr<std::unique_ptr<BoundedMean<double>>> bm =
builder
.SetLaplaceMechanism(absl::make_unique<ZeroNoiseMechanism::Builder>())
- .SetLower(0)
+ .SetLower(-10)
.SetUpper(10)
- .Build()
- .ValueOrDie();
+ .Build();
+ ASSERT_OK(bm);
BoundedMeanTestPeer::AddMultipleEntries<double>(
- 1, std::numeric_limits<uint64_t>::max(), bm.get());
- bm->AddEntry(1);
- bm->AddEntry(1);
+ 0, std::numeric_limits<uint64_t>::max(), (*bm).get());
+ (*bm)->AddEntry(1);
+ (*bm)->AddEntry(1);
- auto result = bm->PartialResult();
- EXPECT_OK(result.status());
- EXPECT_DOUBLE_EQ(
- GetValue<double>(result.value()),
- (static_cast<double>(std::numeric_limits<uint64_t>::max()) + 2) /
- std::numeric_limits<uint64_t>::max());
+ base::StatusOr<Output> result = (*bm)->PartialResult();
+ ASSERT_OK(result);
+ // If the uint64_t raw_count_ overflows, it should be 1, resulting in a mean of
+ // ((0 * uint64_max)+1+1) / 1 = 2, instead of the correct mean of nearly 0.
+ EXPECT_DOUBLE_EQ(GetValue<double>(result.value()), 2);
}
TEST(BoundedMeanTest, OverflowAddMultipleEntriesManualBoundsTest) {
typename BoundedMean<int64_t>::Builder builder;
- std::unique_ptr<BoundedMean<int64_t>> bm =
+ base::StatusOr<std::unique_ptr<BoundedMean<int64_t>>> bm =
builder
.SetLaplaceMechanism(absl::make_unique<ZeroNoiseMechanism::Builder>())
.SetLower(-std::numeric_limits<int64_t>::max() / 2)
.SetUpper(std::numeric_limits<int64_t>::max() / 2)
- .Build()
- .ValueOrDie();
+ .Build();
+ ASSERT_OK(bm);
BoundedMeanTestPeer::AddMultipleEntries<int64_t>(
- 2, std::numeric_limits<int64_t>::max(), bm.get());
+ 2, std::numeric_limits<int64_t>::max(), (*bm).get());
- auto result = bm->PartialResult();
- EXPECT_OK(result.status());
- // Expect 1, since 2 * int64_max should be capped at int64_max by
- // SafeMultiply(), resulting in a mean of int64_max / int64_max = 1.
- EXPECT_DOUBLE_EQ(GetValue<double>(result.value()), 1.0);
+ base::StatusOr<Output> result = (*bm)->PartialResult();
+ ASSERT_OK(result);
+ // Expect -2 / int64_max, since 2 * int64_max should overflow around to -2,
+ // resulting in a mean of -2 / int64_max.
+ EXPECT_DOUBLE_EQ(GetValue<double>(result.value()),
+ -2.0 / std::numeric_limits<int64_t>::max());
}
TEST(BoundedMeanTest, OverflowAddEntryManualBoundsTest) {
typename BoundedMean<int64_t>::Builder builder;
- std::unique_ptr<BoundedMean<int64_t>> bm =
+ base::StatusOr<std::unique_ptr<BoundedMean<int64_t>>> bm =
builder
.SetLaplaceMechanism(absl::make_unique<ZeroNoiseMechanism::Builder>())
.SetLower(0)
.SetUpper(std::numeric_limits<int64_t>::max())
- .Build()
- .ValueOrDie();
- bm->AddEntry(std::numeric_limits<int64_t>::max());
- bm->AddEntry(1);
- bm->AddEntry(1);
- bm->AddEntry(std::numeric_limits<int64_t>::max());
+ .Build();
+ ASSERT_OK(bm);
+ (*bm)->AddEntry(std::numeric_limits<int64_t>::max());
+ (*bm)->AddEntry(1);
+ (*bm)->AddEntry(1);
+ (*bm)->AddEntry(std::numeric_limits<int64_t>::max());
- auto result = bm->PartialResult();
- EXPECT_OK(result.status());
- EXPECT_DOUBLE_EQ(GetValue<double>(result.value()),
- std::numeric_limits<int64_t>::max() / 4);
+ base::StatusOr<Output> result = (*bm)->PartialResult();
+ EXPECT_OK(result);
+ // Overflowing should result in the running sum wrapping around to zero.
+ EXPECT_DOUBLE_EQ(GetValue<double>(result.value()), 0);
}
TEST(BoundedMeanTest, UnderflowAddEntryManualBoundsTest) {
typename BoundedMean<int64_t>::Builder builder;
- std::unique_ptr<BoundedMean<int64_t>> bm =
+ base::StatusOr<std::unique_ptr<BoundedMean<int64_t>>> bm =
builder
.SetLaplaceMechanism(absl::make_unique<ZeroNoiseMechanism::Builder>())
.SetLower(std::numeric_limits<int64_t>::lowest())
.SetUpper(0)
- .Build()
- .ValueOrDie();
- bm->AddEntry(std::numeric_limits<int64_t>::lowest());
- bm->AddEntry(-1);
- bm->AddEntry(-1);
- bm->AddEntry(std::numeric_limits<int64_t>::lowest());
+ .Build();
+ ASSERT_OK(bm);
+ (*bm)->AddEntry(std::numeric_limits<int64_t>::lowest());
+ (*bm)->AddEntry(-1);
+ (*bm)->AddEntry(-1);
+ (*bm)->AddEntry(std::numeric_limits<int64_t>::lowest());
- auto result = bm->PartialResult();
- EXPECT_OK(result.status());
- EXPECT_DOUBLE_EQ(GetValue<double>(result.value()),
- std::numeric_limits<int64_t>::lowest() / 4);
+ base::StatusOr<Output> result = (*bm)->PartialResult();
+ EXPECT_OK(result);
+ // Underflowing should result in the running sum wrapping around to zero.
+ EXPECT_DOUBLE_EQ(GetValue<double>(result.value()), 0);
}
TEST(BoundedMeanTest, OverflowRawCountMergeManualBoundsTest) {
typename BoundedMean<double>::Builder builder;
- std::unique_ptr<BoundedMean<double>> bm =
+ base::StatusOr<std::unique_ptr<BoundedMean<double>>> bm =
builder
.SetLaplaceMechanism(absl::make_unique<ZeroNoiseMechanism::Builder>())
- .SetLower(-std::numeric_limits<double>::max() / 2)
- .SetUpper(std::numeric_limits<double>::max() / 2)
- .Build()
- .ValueOrDie();
+ .SetLower(-10)
+ .SetUpper(10)
+ .Build();
+ ASSERT_OK(bm);
BoundedMeanTestPeer::AddMultipleEntries<double>(
- 1, std::numeric_limits<uint64_t>::max(), bm.get());
- Summary summary = bm->Serialize();
+ 0, std::numeric_limits<uint64_t>::max(), (*bm).get());
+ Summary summary = (*bm)->Serialize();
- std::unique_ptr<BoundedMean<double>> bm2 = builder.Build().ValueOrDie();
- bm2->AddEntry(1);
- bm2->AddEntry(1);
+ base::StatusOr<std::unique_ptr<BoundedMean<double>>> bm2 = builder.Build();
+ ASSERT_OK(bm2);
+ (*bm2)->AddEntry(1);
+ (*bm2)->AddEntry(1);
- EXPECT_OK(bm2->Merge(summary));
+ ASSERT_OK((*bm2)->Merge(summary));
- auto result = bm2->PartialResult();
- EXPECT_OK(result.status());
- EXPECT_DOUBLE_EQ(
- GetValue<double>(result.value()),
- (static_cast<double>(std::numeric_limits<uint64_t>::max()) + 2) /
- std::numeric_limits<uint64_t>::max());
+ base::StatusOr<Output> result = (*bm2)->PartialResult();
+ ASSERT_OK(result);
+ // If the uint64_t raw_count_ overflows, it should be 1, resulting in a mean of
+ // ((0 * uint64_max)+1+1) / 1 = 2, instead of the correct mean of nearly 0.
+ EXPECT_DOUBLE_EQ(GetValue<double>(result.value()), 2);
+
+ // Test post-overflow serialize & merge
+ summary = (*bm2)->Serialize();
+ bm2 = builder.Build();
+ EXPECT_OK((*bm2)->Merge(summary));
+ result = (*bm2)->PartialResult();
+ EXPECT_OK(result);
+ EXPECT_DOUBLE_EQ(GetValue<double>(result.value()), 2);
}
TEST(BoundedMeanTest, OverflowMergeManualBoundsTest) {
typename BoundedMean<int64_t>::Builder builder;
- std::unique_ptr<BoundedMean<int64_t>> bm =
+ base::StatusOr<std::unique_ptr<BoundedMean<int64_t>>> bm =
builder
.SetLaplaceMechanism(absl::make_unique<ZeroNoiseMechanism::Builder>())
.SetLower(0)
.SetUpper(std::numeric_limits<int64_t>::max())
- .Build()
- .ValueOrDie();
- bm->AddEntry(std::numeric_limits<int64_t>::max());
- Summary summary = bm->Serialize();
+ .Build();
+ ASSERT_OK(bm);
+ (*bm)->AddEntry(std::numeric_limits<int64_t>::max());
+ Summary summary = (*bm)->Serialize();
- std::unique_ptr<BoundedMean<int64_t>> bm2 = builder.Build().ValueOrDie();
- bm2->AddEntry(1);
- bm2->AddEntry(1);
- bm2->AddEntry(1);
+ base::StatusOr<std::unique_ptr<BoundedMean<int64_t>>> bm2 = builder.Build();
+ (*bm2)->AddEntry(1);
+ (*bm2)->AddEntry(1);
+ (*bm2)->AddEntry(std::numeric_limits<int64_t>::max());
- EXPECT_OK(bm2->Merge(summary));
+ ASSERT_OK((*bm2)->Merge(summary));
- auto result = bm2->PartialResult();
- EXPECT_OK(result.status());
- EXPECT_DOUBLE_EQ(GetValue<double>(result.value()),
- std::numeric_limits<int64_t>::max() / 4);
+ base::StatusOr<Output> result = (*bm2)->PartialResult();
+ EXPECT_OK(result);
+ // Overflowing should result in the running sum wrapping around to zero.
+ EXPECT_DOUBLE_EQ(GetValue<double>(result.value()), 0.0);
+
+ // Test post-overflow serialize & merge
+ summary = (*bm2)->Serialize();
+ bm2 = builder.Build();
+ EXPECT_OK((*bm2)->Merge(summary));
+ result = (*bm2)->PartialResult();
+ EXPECT_OK(result);
+ EXPECT_DOUBLE_EQ(GetValue<double>(result.value()), 0.0);
}
TEST(BoundedMeanTest, UnderflowMergeManualBoundsTest) {
typename BoundedMean<int64_t>::Builder builder;
- std::unique_ptr<BoundedMean<int64_t>> bm =
+ base::StatusOr<std::unique_ptr<BoundedMean<int64_t>>> bm =
builder
.SetLaplaceMechanism(absl::make_unique<ZeroNoiseMechanism::Builder>())
- .SetLower(std::numeric_limits<int64_t>::lowest() + 1)
+ .SetLower(std::numeric_limits<int64_t>::lowest())
.SetUpper(0)
- .Build()
- .ValueOrDie();
- bm->AddEntry(std::numeric_limits<int64_t>::lowest());
- Summary summary = bm->Serialize();
+ .Build();
+ ASSERT_OK(bm);
+ (*bm)->AddEntry(std::numeric_limits<int64_t>::lowest());
+ Summary summary = (*bm)->Serialize();
- std::unique_ptr<BoundedMean<int64_t>> bm2 = builder.Build().ValueOrDie();
- bm2->AddEntry(-1);
- bm2->AddEntry(-1);
- bm2->AddEntry(-1);
+ base::StatusOr<std::unique_ptr<BoundedMean<int64_t>>> bm2 = builder.Build();
+ (*bm2)->AddEntry(-1);
+ (*bm2)->AddEntry(-1);
+ (*bm2)->AddEntry(std::numeric_limits<int64_t>::lowest());
- EXPECT_OK(bm2->Merge(summary));
+ ASSERT_OK((*bm2)->Merge(summary));
- auto result = bm2->PartialResult();
- EXPECT_OK(result.status());
- EXPECT_DOUBLE_EQ(GetValue<double>(result.value()),
- std::numeric_limits<int64_t>::lowest() / 4);
+ base::StatusOr<Output> result = (*bm2)->PartialResult();
+ EXPECT_OK(result);
+ // Underflowing should result in the running sum wrapping around to zero.
+ EXPECT_DOUBLE_EQ(GetValue<double>(result.value()), 0.0);
+
+ // Test post-overflow serialize & merge
+ summary = (*bm2)->Serialize();
+ bm2 = builder.Build();
+ EXPECT_OK((*bm2)->Merge(summary));
+ result = (*bm2)->PartialResult();
+ EXPECT_OK(result);
+ EXPECT_DOUBLE_EQ(GetValue<double>(result.value()), 0.0);
}
TYPED_TEST(BoundedMeanTest, SerializeMergeTest) {
diff --git a/cc/algorithms/bounded-standard-deviation.h b/cc/algorithms/bounded-standard-deviation.h
index 56b0baf..1785745 100644
--- a/cc/algorithms/bounded-standard-deviation.h
+++ b/cc/algorithms/bounded-standard-deviation.h
@@ -118,6 +118,9 @@
base::StatusOr<Output> GenerateResult(double privacy_budget,
double noise_interval_level) override {
+ RETURN_IF_ERROR(ValidateIsPositive(privacy_budget, "Privacy budget",
+ absl::StatusCode::kFailedPrecondition));
+
ASSIGN_OR_RETURN(
Output variance_output,
variance_->PartialResult(privacy_budget, noise_interval_level));
diff --git a/cc/algorithms/bounded-standard-deviation_test.cc b/cc/algorithms/bounded-standard-deviation_test.cc
index 8db2472..0d1e295 100644
--- a/cc/algorithms/bounded-standard-deviation_test.cc
+++ b/cc/algorithms/bounded-standard-deviation_test.cc
@@ -31,6 +31,8 @@
using ::differential_privacy::test_utils::ZeroNoiseMechanism;
using ::differential_privacy::base::testing::EqualsProto;
+using ::testing::HasSubstr;
+using ::differential_privacy::base::testing::StatusIs;
template <typename T>
class BoundedStandardDeviationTest : public testing::Test {
@@ -75,6 +77,24 @@
GetValue<double>(bsd->PartialResult(0.5).ValueOrDie()));
}
+TYPED_TEST(BoundedStandardDeviationTest, InsufficientPrivacyBudgetTest) {
+ std::vector<TypeParam> a = {1, 5, 7, 9, 13};
+ std::unique_ptr<BoundedStandardDeviation<TypeParam>> bsd =
+ typename BoundedStandardDeviation<TypeParam>::Builder()
+ .SetLaplaceMechanism(absl::make_unique<ZeroNoiseMechanism::Builder>())
+ .SetEpsilon(1)
+ .SetLower(0)
+ .SetUpper(15)
+ .Build()
+ .ValueOrDie();
+ bsd->AddEntries(a.begin(), a.end());
+
+ ASSERT_OK(bsd->PartialResult());
+ EXPECT_THAT(bsd->PartialResult(),
+ StatusIs(absl::StatusCode::kFailedPrecondition,
+ HasSubstr("Privacy budget must be positive")));
+}
+
TYPED_TEST(BoundedStandardDeviationTest, ClampInputTest) {
std::vector<TypeParam> a = {0, 0, 10, 10};
std::unique_ptr<BoundedStandardDeviation<TypeParam>> bsd =
diff --git a/cc/algorithms/bounded-sum.h b/cc/algorithms/bounded-sum.h
index a0e62de..deee724 100644
--- a/cc/algorithms/bounded-sum.h
+++ b/cc/algorithms/bounded-sum.h
@@ -62,9 +62,8 @@
override {
// We have to check epsilon now, otherwise the split during ApproxBounds
// construction might make the error message confusing.
- RETURN_IF_ERROR(
- GetValueIfSetAndPositive(AlgorithmBuilder::GetEpsilon(), "Epsilon")
- .status());
+ RETURN_IF_ERROR(ValidateIsFiniteAndPositive(
+ AlgorithmBuilder::GetEpsilon(), "Epsilon"));
// Ensure that either bounds are manually set or ApproxBounds is made.
RETURN_IF_ERROR(BoundedBuilder::BoundsSetup());
@@ -108,7 +107,7 @@
// If manual bounds are set, clamp immediately and store sum. Otherwise,
// feed inputs into ApproxBounds and store temporary partial sums.
if (!approx_bounds_) {
- SafeAdd(pos_sum_[0], Clamp<T>(lower_, upper_, t), &pos_sum_[0]);
+ pos_sum_[0] += Clamp<T>(lower_, upper_, t);
} else {
approx_bounds_->AddEntry(t);
@@ -175,10 +174,10 @@
"values as this BoundedSum.");
}
for (int i = 0; i < pos_sum_.size(); ++i) {
- SafeAdd(pos_sum_[i], GetValue<T>(bs_summary.pos_sum(i)), &pos_sum_[i]);
+ pos_sum_[i] += GetValue<T>(bs_summary.pos_sum(i));
}
for (int i = 0; i < neg_sum_.size(); ++i) {
- SafeAdd(neg_sum_[i], GetValue<T>(bs_summary.neg_sum(i)), &neg_sum_[i]);
+ neg_sum_[i] += GetValue<T>(bs_summary.neg_sum(i));
}
if (approx_bounds_) {
Summary approx_bounds_summary;
@@ -252,9 +251,8 @@
base::StatusOr<Output> GenerateResult(double privacy_budget,
double noise_interval_level) override {
- DCHECK_GT(privacy_budget, 0.0)
- << "Privacy budget should be greater than zero.";
- if (privacy_budget == 0.0) return Output();
+ RETURN_IF_ERROR(ValidateIsPositive(privacy_budget, "Privacy budget",
+ absl::StatusCode::kFailedPrecondition));
Output output;
double sum = 0;
diff --git a/cc/algorithms/bounded-sum_test.cc b/cc/algorithms/bounded-sum_test.cc
index 030a791..52c0a81 100644
--- a/cc/algorithms/bounded-sum_test.cc
+++ b/cc/algorithms/bounded-sum_test.cc
@@ -33,7 +33,9 @@
using ::differential_privacy::test_utils::ZeroNoiseMechanism;
using ::testing::Eq;
using ::differential_privacy::base::testing::EqualsProto;
+using ::testing::HasSubstr;
using ::differential_privacy::base::testing::IsOkAndHolds;
+using ::differential_privacy::base::testing::StatusIs;
constexpr double kNumSamples = 10000;
@@ -89,6 +91,23 @@
typename BoundedSum<TypeParam>::Builder()
.SetLaplaceMechanism(absl::make_unique<ZeroNoiseMechanism::Builder>())
.SetEpsilon(1.0)
+ .SetLower(0)
+ .SetUpper(10)
+ .Build();
+ ASSERT_OK(bs);
+ (*bs)->AddEntries(a.begin(), a.end());
+ ASSERT_OK((*bs)->PartialResult());
+ EXPECT_THAT((*bs)->PartialResult(),
+ StatusIs(absl::StatusCode::kFailedPrecondition,
+ HasSubstr("Privacy budget must be positive")));
+}
+
+TYPED_TEST(BoundedSumTest, InsufficientPrivacyBudgetTest) {
+ std::vector<TypeParam> a = {1, 2, 3, 4};
+ auto bs =
+ typename BoundedSum<TypeParam>::Builder()
+ .SetLaplaceMechanism(absl::make_unique<ZeroNoiseMechanism::Builder>())
+ .SetEpsilon(1.0)
.SetLower(1)
.SetUpper(2)
.Build();
@@ -311,93 +330,113 @@
TEST(BoundedSumTest, OverflowAddEntryManualBounds) {
typename BoundedSum<int64_t>::Builder builder;
- std::unique_ptr<BoundedSum<int64_t>> bs =
+ base::StatusOr<std::unique_ptr<BoundedSum<int64_t>>> bs =
builder
.SetLaplaceMechanism(absl::make_unique<ZeroNoiseMechanism::Builder>())
.SetLower(0)
.SetUpper(std::numeric_limits<int64_t>::max())
- .Build()
- .ValueOrDie();
- bs->AddEntry(std::numeric_limits<int64_t>::max());
- bs->AddEntry(1);
- bs->AddEntry(1);
- bs->AddEntry(std::numeric_limits<int64_t>::max());
+ .Build();
+ ASSERT_OK(bs);
+ (*bs)->AddEntry(std::numeric_limits<int64_t>::max());
+ (*bs)->AddEntry(1);
+ (*bs)->AddEntry(1);
+ (*bs)->AddEntry(std::numeric_limits<int64_t>::max());
- auto result = bs->PartialResult();
- EXPECT_OK(result.status());
- EXPECT_EQ(GetValue<int64_t>(result.value()), std::numeric_limits<int64_t>::max());
+ base::StatusOr<Output> result = (*bs)->PartialResult();
+ ASSERT_OK(result);
+ // Overflowing should result in the running sum wrapping around to zero.
+ EXPECT_EQ(GetValue<int64_t>(result.value()), 0);
}
TEST(BoundedSumTest, UnderflowAddEntryManualBounds) {
typename BoundedSum<int64_t>::Builder builder;
- std::unique_ptr<BoundedSum<int64_t>> bs =
+ base::StatusOr<std::unique_ptr<BoundedSum<int64_t>>> bs =
builder
.SetLaplaceMechanism(absl::make_unique<ZeroNoiseMechanism::Builder>())
.SetLower(std::numeric_limits<int64_t>::lowest() + 1)
.SetUpper(0)
.Build()
.ValueOrDie();
- bs->AddEntry(std::numeric_limits<int64_t>::lowest());
- bs->AddEntry(-1);
- bs->AddEntry(-1);
- bs->AddEntry(std::numeric_limits<int64_t>::lowest());
+ (*bs)->AddEntry(std::numeric_limits<int64_t>::lowest() + 1);
+ (*bs)->AddEntry(-1);
+ (*bs)->AddEntry(-1);
+ (*bs)->AddEntry(std::numeric_limits<int64_t>::lowest() + 1);
- auto result = bs->PartialResult();
- EXPECT_OK(result.status());
- EXPECT_EQ(GetValue<int64_t>(result.value()),
- std::numeric_limits<int64_t>::lowest());
+ base::StatusOr<Output> result = (*bs)->PartialResult();
+ ASSERT_OK(result);
+ // Underflowing should result in the running sum wrapping around to zero.
+ EXPECT_EQ(GetValue<int64_t>(result.value()), 0);
}
TEST(BoundedSumTest, OverflowMergeManualBoundsTest) {
typename BoundedSum<int64_t>::Builder builder;
- std::unique_ptr<BoundedSum<int64_t>> bs =
+ base::StatusOr<std::unique_ptr<BoundedSum<int64_t>>> bs =
builder
.SetLaplaceMechanism(absl::make_unique<ZeroNoiseMechanism::Builder>())
.SetLower(0)
.SetUpper(std::numeric_limits<int64_t>::max())
- .Build()
- .ValueOrDie();
- bs->AddEntry(std::numeric_limits<int64_t>::max());
- Summary summary = bs->Serialize();
+ .Build();
+ ASSERT_OK(bs);
+ (*bs)->AddEntry(std::numeric_limits<int64_t>::max());
+ Summary summary = (*bs)->Serialize();
- std::unique_ptr<BoundedSum<int64_t>> bs2 = builder.Build().ValueOrDie();
- bs2->AddEntry(1);
- bs2->AddEntry(1);
- bs2->AddEntry(1);
+ base::StatusOr<std::unique_ptr<BoundedSum<int64_t>>> bs2 = builder.Build();
+ ASSERT_OK(bs2);
+ (*bs2)->AddEntry(1);
+ (*bs2)->AddEntry(1);
+ (*bs2)->AddEntry(std::numeric_limits<int64_t>::max());
- EXPECT_OK(bs2->Merge(summary));
+ ASSERT_OK((*bs2)->Merge(summary));
- auto result = bs2->PartialResult();
- EXPECT_OK(result.status());
- EXPECT_EQ(GetValue<int64_t>(result.value()), std::numeric_limits<int64_t>::max());
+ base::StatusOr<Output> result = (*bs2)->PartialResult();
+ EXPECT_OK(result);
+ // Overflowing should result in the running sum wrapping around to zero.
+ EXPECT_EQ(GetValue<int64_t>(result.value()), 0);
+
+ // Test post-overflow serialize & merge
+ summary = (*bs2)->Serialize();
+ bs2 = builder.Build();
+ EXPECT_OK((*bs2)->Merge(summary));
+ result = (*bs2)->PartialResult();
+ EXPECT_OK(result);
+ EXPECT_DOUBLE_EQ(GetValue<double>(result.value()), 0);
}
TEST(BoundedSumTest, UnderflowMergeManualBoundsTest) {
typename BoundedSum<int64_t>::Builder builder;
- std::unique_ptr<BoundedSum<int64_t>> bs =
+ base::StatusOr<std::unique_ptr<BoundedSum<int64_t>>> bs =
builder
.SetLaplaceMechanism(absl::make_unique<ZeroNoiseMechanism::Builder>())
.SetLower(std::numeric_limits<int64_t>::lowest() + 1)
.SetUpper(0)
.Build()
.ValueOrDie();
- bs->AddEntry(std::numeric_limits<int64_t>::lowest());
- Summary summary = bs->Serialize();
+ (*bs)->AddEntry(std::numeric_limits<int64_t>::lowest() + 1);
+ Summary summary = (*bs)->Serialize();
- std::unique_ptr<BoundedSum<int64_t>> bs2 = builder.Build().ValueOrDie();
- bs2->AddEntry(-1);
- bs2->AddEntry(-1);
- bs2->AddEntry(-1);
+ base::StatusOr<std::unique_ptr<BoundedSum<int64_t>>> bs2 = builder.Build();
+ ASSERT_OK(bs2);
+ (*bs2)->AddEntry(-1);
+ (*bs2)->AddEntry(-1);
+ (*bs2)->AddEntry(std::numeric_limits<int64_t>::lowest() + 1);
- EXPECT_OK(bs2->Merge(summary));
+ EXPECT_OK((*bs2)->Merge(summary));
- auto result = bs2->PartialResult();
- EXPECT_OK(result.status());
- EXPECT_EQ(GetValue<int64_t>(result.value()),
- std::numeric_limits<int64_t>::lowest());
+ base::StatusOr<Output> result = (*bs2)->PartialResult();
+ EXPECT_OK(result);
+ // Underflowing should result in the running sum wrapping around to zero.
+ EXPECT_EQ(GetValue<int64_t>(result.value()), 0);
+
+ // Test post-overflow serialize & merge
+ summary = (*bs2)->Serialize();
+ bs2 = builder.Build();
+ EXPECT_OK((*bs2)->Merge(summary));
+ result = (*bs2)->PartialResult();
+ EXPECT_OK(result);
+ EXPECT_DOUBLE_EQ(GetValue<double>(result.value()), 0);
}
TEST(BoundedSumTest, DropNanEntriesManualBounds) {
diff --git a/cc/algorithms/bounded-variance.h b/cc/algorithms/bounded-variance.h
index 120e21d..a09d1da 100644
--- a/cc/algorithms/bounded-variance.h
+++ b/cc/algorithms/bounded-variance.h
@@ -96,9 +96,8 @@
override {
// We have to check epsilon now, otherwise the split during ApproxBounds
// construction might make the error message confusing.
- RETURN_IF_ERROR(
- GetValueIfSetAndPositive(AlgorithmBuilder::GetEpsilon(), "Epsilon")
- .status());
+ RETURN_IF_ERROR(ValidateIsFiniteAndPositive(
+ AlgorithmBuilder::GetEpsilon(), "Epsilon"));
// Ensure that either bounds are manually set or ApproxBounds is made.
RETURN_IF_ERROR(BoundedBuilder::BoundsSetup());
@@ -212,13 +211,13 @@
}
// Add count and partial values to current ones.
- SafeAdd(raw_count_, bv_summary.count(), &raw_count_);
+ raw_count_ += bv_summary.count();
for (int i = 0; i < pos_sum_.size(); ++i) {
- SafeAdd(pos_sum_[i], GetValue<T>(bv_summary.pos_sum(i)), &pos_sum_[i]);
+ pos_sum_[i] += GetValue<T>(bv_summary.pos_sum(i));
pos_sum_of_squares_[i] += bv_summary.pos_sum_of_squares(i);
}
for (int i = 0; i < neg_sum_.size(); ++i) {
- SafeAdd(neg_sum_[i], GetValue<T>(bv_summary.neg_sum(i)), &neg_sum_[i]);
+ neg_sum_[i] += GetValue<T>(bv_summary.neg_sum(i));
neg_sum_of_squares_[i] += bv_summary.neg_sum_of_squares(i);
}
@@ -306,9 +305,9 @@
base::StatusOr<Output> GenerateResult(double privacy_budget,
double noise_interval_level) override {
- DCHECK_GT(privacy_budget, 0.0)
- << "Privacy budget should be greater than zero.";
- if (privacy_budget == 0.0) return Output();
+ RETURN_IF_ERROR(ValidateIsPositive(privacy_budget, "Privacy budget",
+ absl::StatusCode::kFailedPrecondition));
+
double remaining_budget = privacy_budget;
Output output;
@@ -480,7 +479,7 @@
}
// Count is unaffected by clamping.
- SafeAdd<uint64_t>(raw_count_, num_of_entries, &raw_count_);
+ raw_count_ += num_of_entries;
// If bounds exist, clamp and record. Otherwise, store partial results and
// feed input into ApproxBounds algorithm.
@@ -517,10 +516,8 @@
"AddManualBoundsEntry() can only be used when bounds were set "
"manually.");
}
- SafeAdd(pos_sum_[0],
- Clamp<T>(std::numeric_limits<T>::lowest(),
- std::numeric_limits<T>::max(), t * num_of_entries),
- &pos_sum_[0]);
+ pos_sum_[0] += Clamp<T>(std::numeric_limits<T>::lowest(),
+ std::numeric_limits<T>::max(), t * num_of_entries);
pos_sum_of_squares_[0] += pow(t, 2) * num_of_entries;
return absl::OkStatus();
}
diff --git a/cc/algorithms/bounded-variance_test.cc b/cc/algorithms/bounded-variance_test.cc
index 31c1fed..c7511d8 100644
--- a/cc/algorithms/bounded-variance_test.cc
+++ b/cc/algorithms/bounded-variance_test.cc
@@ -16,8 +16,6 @@
#include "algorithms/bounded-variance.h"
-#include <limits>
-
#include "base/testing/proto_matchers.h"
#include "base/testing/status_matchers.h"
#include "gmock/gmock.h"
@@ -135,6 +133,23 @@
EXPECT_DOUBLE_EQ(GetValue<double>(*result1), GetValue<double>(*result2));
}
+TYPED_TEST(BoundedVarianceTest, InsufficientPrivacyBudgetTest) {
+ std::vector<TypeParam> a = {1, 2, 3, 4, 5};
+ base::StatusOr<std::unique_ptr<BoundedVariance<TypeParam>>> bv =
+ typename BoundedVariance<TypeParam>::Builder()
+ .SetLaplaceMechanism(absl::make_unique<ZeroNoiseMechanism::Builder>())
+ .SetEpsilon(1.0)
+ .SetLower(0)
+ .SetUpper(6)
+ .Build();
+ ASSERT_OK(bv);
+ (*bv)->AddEntries(a.begin(), a.end());
+ ASSERT_OK((*bv)->PartialResult());
+ EXPECT_THAT((*bv)->PartialResult(),
+ StatusIs(absl::StatusCode::kFailedPrecondition,
+ HasSubstr("Privacy budget must be positive")));
+}
+
TYPED_TEST(BoundedVarianceTest, ClampInputTest) {
std::vector<TypeParam> a = {0, 0, 10, 10};
base::StatusOr<std::unique_ptr<BoundedVariance<TypeParam>>> bv =
@@ -377,8 +392,8 @@
auto result = bv->PartialResult();
EXPECT_OK(result.status());
- // A raw_count_ overflow would result in a larger variance of 1.
- EXPECT_DOUBLE_EQ(GetValue<double>(result.value()), 0.25);
+ // A raw_count_ overflow should result in a variance of 1.0.
+ EXPECT_DOUBLE_EQ(GetValue<double>(result.value()), 1.0);
}
TEST(BoundedVarianceTest, OverflowAddEntryManualBounds) {
@@ -399,8 +414,8 @@
auto result = bv->PartialResult();
EXPECT_OK(result.status());
- // Overflow would result in a variance of 1.0
- EXPECT_DOUBLE_EQ(GetValue<double>(result.value()), 0.75);
+ // Overflow should result in a variance of 1.0.
+ EXPECT_DOUBLE_EQ(GetValue<double>(result.value()), 1.0);
}
TEST(BoundedVarianceTest, UnderflowAddEntryManualBounds) {
@@ -420,8 +435,8 @@
auto result = bv->PartialResult();
EXPECT_OK(result.status());
- // Overflow would result in a variance of 1.0
- EXPECT_DOUBLE_EQ(GetValue<double>(result.value()), 0.75);
+ // Overflow should result in a variance of 1.0
+ EXPECT_DOUBLE_EQ(GetValue<double>(result.value()), 1.0);
}
TEST(BoundedVarianceTest, OverflowRawCountMergeManualBoundsTest) {
@@ -447,10 +462,12 @@
auto result = bv2->PartialResult();
EXPECT_OK(result.status());
- // An overflow would cause the count of entries to be 1, which would result in
- // the variance being based entirely on the midpoint between the upper and
- // lower bounds, instead of based upon the actual data entries.
- EXPECT_DOUBLE_EQ(GetValue<double>(result.value()), 0);
+ // An overflow should cause the count of entries to be 1, which should result
+ // in the variance so large that it becomes clamped to
+ // IntervalLengthSquared(lower, upper) / 4, instead of based upon the actual
+ // data entries (which would be 0 if there was no count overflow, since all
+ // entries are the same and do not vary).
+ EXPECT_DOUBLE_EQ(GetValue<double>(result.value()), 25.0);
}
TEST(BoundedVarianceTest, OverflowMergeManualBoundsTest) {
@@ -475,8 +492,8 @@
auto result = bv2->PartialResult();
EXPECT_OK(result.status());
- // Overflow would result in a variance of 1.0
- EXPECT_DOUBLE_EQ(GetValue<double>(result.value()), 0.75);
+ // Overflow should result in a variance of 1.0
+ EXPECT_DOUBLE_EQ(GetValue<double>(result.value()), 1.0);
}
TEST(BoundedVarianceTest, UnderflowMergeManualBoundsTest) {
@@ -501,8 +518,8 @@
auto result = bv2->PartialResult();
EXPECT_OK(result.status());
- // Overflow would result in a variance of 1.0
- EXPECT_DOUBLE_EQ(GetValue<double>(result.value()), 0.75);
+ // Underflow should result in a variance of 1.0
+ EXPECT_DOUBLE_EQ(GetValue<double>(result.value()), 1.0);
}
TEST(BoundedVarianceTest, SensitivityOverflow) {
diff --git a/cc/algorithms/count.h b/cc/algorithms/count.h
index 00ab963..da8f174 100644
--- a/cc/algorithms/count.h
+++ b/cc/algorithms/count.h
@@ -66,7 +66,7 @@
if (!summary.data().UnpackTo(&count_summary)) {
return absl::InternalError("Count summary unable to be unpacked.");
}
- SafeAdd<uint64_t>(count_, count_summary.count(), &count_);
+ count_ += count_summary.count();
return absl::OkStatus();
}
@@ -82,6 +82,9 @@
protected:
base::StatusOr<Output> GenerateResult(double privacy_budget,
double noise_interval_level) override {
+ RETURN_IF_ERROR(ValidateIsPositive(privacy_budget, "Privacy budget",
+ absl::StatusCode::kFailedPrecondition));
+
Output output;
int64_t countWithNoise;
SafeCastFromDouble(std::round(mechanism_->AddNoise(count_, privacy_budget)),
@@ -107,7 +110,7 @@
private:
void AddMultipleEntries(const T& v, uint64_t num_of_entries) {
- SafeAdd<uint64_t>(count_, num_of_entries, &count_);
+ count_ += num_of_entries;
}
// Friend class for testing only
diff --git a/cc/algorithms/count_test.cc b/cc/algorithms/count_test.cc
index 1e30a93..86bb29e 100644
--- a/cc/algorithms/count_test.cc
+++ b/cc/algorithms/count_test.cc
@@ -45,7 +45,9 @@
using ::differential_privacy::test_utils::ZeroNoiseMechanism;
using ::differential_privacy::base::testing::EqualsProto;
+using ::testing::HasSubstr;
using ::differential_privacy::base::testing::IsOkAndHolds;
+using ::differential_privacy::base::testing::StatusIs;
template <typename T>
class CountTest : public testing::Test {};
@@ -83,6 +85,22 @@
EXPECT_EQ(GetValue<int64_t>(*result1), GetValue<int64_t>(*result2));
}
+TYPED_TEST(CountTest, InsufficientPrivacyBudgetTest) {
+ std::vector<TypeParam> c = {1, 2, 3, 4, 2, 3};
+ auto count =
+ typename Count<TypeParam>::Builder()
+ .SetLaplaceMechanism(absl::make_unique<ZeroNoiseMechanism::Builder>())
+ .Build();
+ ASSERT_OK(count);
+
+ (*count)->AddEntries(c.begin(), c.end());
+
+ ASSERT_OK((*count)->PartialResult());
+ EXPECT_THAT((*count)->PartialResult(),
+ StatusIs(absl::StatusCode::kFailedPrecondition,
+ HasSubstr("Privacy budget must be positive")));
+}
+
TEST(CountTest, ConfidenceIntervalTest) {
double epsilon = 0.5;
double level = .95;
@@ -115,13 +133,11 @@
CountTestPeer::AddMultipleEntries<uint64_t>(
1, std::numeric_limits<uint64_t>::max(), &**count);
(*count)->AddEntry(1);
- (*count)->AddEntry(1);
- (*count)->AddEntry(1);
auto result = (*count)->PartialResult();
ASSERT_OK(result);
- EXPECT_EQ(GetValue<int64_t>(*result), std::numeric_limits<int64_t>::max());
+ EXPECT_EQ(GetValue<int64_t>(*result), 0);
}
TEST(CountTest, SerializeTest) {
@@ -161,29 +177,30 @@
}
TEST(CountTest, SerializeAndMergeOverflowTest) {
- auto count1 =
- Count<uint64_t>::Builder()
- .SetLaplaceMechanism(absl::make_unique<ZeroNoiseMechanism::Builder>())
- .Build();
+ Count<uint64_t>::Builder builder;
+ builder.SetLaplaceMechanism(absl::make_unique<ZeroNoiseMechanism::Builder>());
+ base::StatusOr<std::unique_ptr<Count<uint64_t>>> count1 = builder.Build();
ASSERT_OK(count1);
CountTestPeer::AddMultipleEntries<uint64_t>(
1, std::numeric_limits<uint64_t>::max(), &**count1);
Summary summary = (*count1)->Serialize();
- auto count2 =
- Count<uint64_t>::Builder()
- .SetLaplaceMechanism(absl::make_unique<ZeroNoiseMechanism::Builder>())
- .Build();
+ base::StatusOr<std::unique_ptr<Count<uint64_t>>> count2 = builder.Build();
ASSERT_OK(count2);
(*count2)->AddEntry(1);
- (*count2)->AddEntry(2);
-
EXPECT_OK((*count2)->Merge(summary));
- auto result = (*count2)->PartialResult();
+ base::StatusOr<Output> result = (*count2)->PartialResult();
ASSERT_OK(result);
+ EXPECT_EQ(GetValue<int64_t>(*result), 0);
- EXPECT_EQ(GetValue<int64_t>(*result), std::numeric_limits<int64_t>::max());
+ // Test post-overflow serialize & merge
+ summary = (*count2)->Serialize();
+ count2 = builder.Build();
+ ASSERT_OK((*count2)->Merge(summary));
+ result = (*count2)->PartialResult();
+ ASSERT_OK(result.status());
+ EXPECT_DOUBLE_EQ(GetValue<int64_t>(result.value()), 0);
}
TEST(CountTest, MemoryUsed) {
diff --git a/cc/algorithms/numerical-mechanisms.h b/cc/algorithms/numerical-mechanisms.h
index a713d7b..b431742 100644
--- a/cc/algorithms/numerical-mechanisms.h
+++ b/cc/algorithms/numerical-mechanisms.h
@@ -83,37 +83,23 @@
virtual int64_t MemoryUsed() = 0;
virtual base::StatusOr<ConfidenceInterval> NoiseConfidenceInterval(
- double confidence_level, double privacy_budget, double noised_result) {
- return absl::UnimplementedError(
- "NoiseConfidenceInterval() unsupported for this numerical mechanism.");
- }
+ double confidence_level, double privacy_budget, double noised_result) = 0;
virtual base::StatusOr<ConfidenceInterval> NoiseConfidenceInterval(
- double confidence_level, double privacy_budget) {
- return absl::UnimplementedError(
- "NoiseConfidenceInterval() unsupported for this numerical mechanism.");
- }
+ double confidence_level, double privacy_budget) = 0;
double GetEpsilon() { return epsilon_; }
protected:
absl::Status CheckConfidenceLevel(double confidence_level) {
- if (std::isnan(confidence_level) ||
- !(0 < confidence_level && confidence_level < 1)) {
- return absl::InvalidArgumentError(absl::StrCat(
- "Confidence level has to be in the open interval (0,1), but is ",
- confidence_level));
- }
+ RETURN_IF_ERROR(ValidateIsInExclusiveInterval(confidence_level, 0, 1,
+ "Confidence level"));
return absl::OkStatus();
}
absl::Status CheckPrivacyBudget(double privacy_budget) {
- if (std::isnan(privacy_budget) ||
- !(0 < privacy_budget && privacy_budget <= 1)) {
- return absl::InvalidArgumentError(absl::StrCat(
- "privacy_budget has to be in the interval (0, 1], but is ",
- privacy_budget));
- }
+ RETURN_IF_ERROR(ValidateIsInInterval(privacy_budget, 0, 1, false, true,
+ "Privacy budget"));
return absl::OkStatus();
}
@@ -167,11 +153,8 @@
protected:
// Checks if delta is set and valid to be used in the Gaussian mechanism.
absl::Status DeltaIsSetAndValid() const {
- ASSIGN_OR_RETURN(double delta, GetValueIfSetAndFinite(delta_, "Delta"));
- if (delta <= 0 || 1 <= delta) {
- return absl::InvalidArgumentError(absl::StrCat(
- "Delta has to be in the interval (0, 1) but is ", delta));
- }
+ RETURN_IF_ERROR(ValidateIsFinite(delta_, "Delta"));
+ RETURN_IF_ERROR(ValidateIsInExclusiveInterval(delta_, 0, 1, "Delta"));
return absl::OkStatus();
}
@@ -208,8 +191,8 @@
}
base::StatusOr<std::unique_ptr<NumericalMechanism>> Build() override {
- ASSIGN_OR_RETURN(double epsilon,
- GetValueIfSetAndPositive(GetEpsilon(), "Epsilon"));
+ RETURN_IF_ERROR(ValidateIsFiniteAndPositive(GetEpsilon(), "Epsilon"));
+ double epsilon = GetEpsilon().value();
ASSIGN_OR_RETURN(double L1, CalculateL1Sensitivity());
// Check that generated noise is not likely to overflow.
double diversity = L1 / epsilon;
@@ -244,14 +227,21 @@
// on the l1 sensitivity calculated from l0 and linf sensitivities.
base::StatusOr<double> CalculateL1Sensitivity() {
if (l1_sensitivity_.has_value()) {
- return GetValueIfSetAndPositive(l1_sensitivity_, "L1 sensitivity");
+ absl::Status status =
+ ValidateIsFiniteAndPositive(l1_sensitivity_, "L1 sensitivity");
+ if (status.ok()) {
+ return l1_sensitivity_.value();
+ } else {
+ return status;
+ }
}
if (GetL0Sensitivity().has_value() && GetLInfSensitivity().has_value()) {
- ASSIGN_OR_RETURN(double l0, GetValueIfSetAndPositive(GetL0Sensitivity(),
- "L0 sensitivity"));
- ASSIGN_OR_RETURN(
- double linf,
- GetValueIfSetAndPositive(GetLInfSensitivity(), "LInf sensitivity"));
+ RETURN_IF_ERROR(
+ ValidateIsFiniteAndPositive(GetL0Sensitivity(), "L0 sensitivity"));
+ double l0 = GetL0Sensitivity().value();
+ RETURN_IF_ERROR(ValidateIsFiniteAndPositive(GetLInfSensitivity(),
+ "LInf sensitivity"));
+ double linf = GetLInfSensitivity().value();
double l1 = l0 * linf;
if (!std::isfinite(l1)) {
return absl::InvalidArgumentError(absl::StrCat(
@@ -358,12 +348,13 @@
}
base::StatusOr<std::unique_ptr<NumericalMechanism>> Build() override {
- ASSIGN_OR_RETURN(double epsilon,
- GetValueIfSetAndPositive(GetEpsilon(), "Epsilon"));
+ absl::optional<double> epsilon = GetEpsilon();
+ RETURN_IF_ERROR(ValidateIsFiniteAndPositive(epsilon, "Epsilon"));
RETURN_IF_ERROR(DeltaIsSetAndValid());
ASSIGN_OR_RETURN(double l2, CalculateL2Sensitivity());
std::unique_ptr<NumericalMechanism> result =
- absl::make_unique<GaussianMechanism>(epsilon, GetDelta().value(), l2);
+ absl::make_unique<GaussianMechanism>(epsilon.value(),
+ GetDelta().value(), l2);
return result;
}
@@ -379,19 +370,26 @@
// on the l2 sensitivity calculated from l0 and linf sensitivities.
base::StatusOr<double> CalculateL2Sensitivity() {
if (l2_sensitivity_.has_value()) {
- return GetValueIfSetAndPositive(l2_sensitivity_, "L2 sensitivity");
+ absl::Status status =
+ ValidateIsFiniteAndPositive(l2_sensitivity_, "L2 sensitivity");
+ if (status.ok()) {
+ return l2_sensitivity_.value();
+ } else {
+ return status;
+ }
} else if (GetL0Sensitivity().has_value() &&
GetLInfSensitivity().has_value()) {
// Try to calculate L2 sensitivity from L0 and LInf sensitivities
- ASSIGN_OR_RETURN(double l0, GetValueIfSetAndPositive(GetL0Sensitivity(),
- "L0 sensitivity"));
- ASSIGN_OR_RETURN(
- double linf,
- GetValueIfSetAndPositive(GetLInfSensitivity(), "LInf sensitivity"));
+ RETURN_IF_ERROR(
+ ValidateIsFiniteAndPositive(GetL0Sensitivity(), "L0 sensitivity"));
+ double l0 = GetL0Sensitivity().value();
+ RETURN_IF_ERROR(ValidateIsFiniteAndPositive(GetLInfSensitivity(),
+ "LInf sensitivity"));
+ double linf = GetLInfSensitivity().value();
double l2 = std::sqrt(l0) * linf;
if (!std::isfinite(l2) || l2 <= 0) {
return absl::InvalidArgumentError(absl::StrCat(
- "The calculated L2 sensitivity has to be positive and finite but "
+ "The calculated L2 sensitivity must be positive and finite but "
"is ",
l2,
". Contribution or sensitivity settings might be too high or too "
diff --git a/cc/algorithms/numerical-mechanisms_test.cc b/cc/algorithms/numerical-mechanisms_test.cc
index 47a517d..a8eb34c 100644
--- a/cc/algorithms/numerical-mechanisms_test.cc
+++ b/cc/algorithms/numerical-mechanisms_test.cc
@@ -70,7 +70,7 @@
// Convert message to std::string so that the matcher works in the open source
// version.
std::string message(failed_build.status().message());
- EXPECT_THAT(message, MatchesRegex("^Epsilon has to be set.*"));
+ EXPECT_THAT(message, MatchesRegex("^Epsilon must be set.*"));
}
TEST(NumericalMechanismsTest, LaplaceBuilderFailsEpsilonZero) {
@@ -81,7 +81,7 @@
// Convert message to std::string so that the matcher works in the open source
// version.
std::string message(failed_build.status().message());
- EXPECT_THAT(message, MatchesRegex("^Epsilon has to be positive.*"));
+ EXPECT_THAT(message, MatchesRegex("^Epsilon must be finite and positive.*"));
}
TEST(NumericalMechanismsTest, LaplaceBuilderFailsEpsilonNegative) {
@@ -92,7 +92,7 @@
// Convert message to std::string so that the matcher works in the open source
// version.
std::string message(failed_build.status().message());
- EXPECT_THAT(message, MatchesRegex("^Epsilon has to be positive.*"));
+ EXPECT_THAT(message, MatchesRegex("^Epsilon must be finite and positive.*"));
}
TEST(NumericalMechanismsTest, LaplaceBuilderFailsEpsilonNan) {
@@ -103,7 +103,8 @@
// Convert message to std::string so that the matcher works in the open source
// version.
std::string message(failed_build.status().message());
- EXPECT_THAT(message, MatchesRegex("^Epsilon has to be finite.*"));
+ EXPECT_THAT(message,
+ MatchesRegex("^Epsilon must be a valid numeric value.*"));
}
TEST(NumericalMechanismsTest, LaplaceBuilderFailsEpsilonInfinity) {
@@ -115,12 +116,60 @@
// Convert message to std::string so that the matcher works in the open source
// version.
std::string message(failed_build.status().message());
- EXPECT_THAT(message, MatchesRegex("^Epsilon has to be finite.*"));
+ EXPECT_THAT(message, MatchesRegex("^Epsilon must be finite.*"));
}
-TEST(NumericalMechanismsTest, LaplaceBuilderFailsL0SensitivityNan) {
+TEST(NumericalMechanismsTest, LaplaceBuilderFailsL1SensitivityZero) {
LaplaceMechanism::Builder test_builder;
- auto failed_build = test_builder.SetL0Sensitivity(NAN)
+ auto failed_build = test_builder.SetL1Sensitivity(0).SetEpsilon(1).Build();
+ EXPECT_THAT(failed_build.status().code(),
+ Eq(absl::StatusCode::kInvalidArgument));
+ // Convert message to std::string so that the matcher works in the open source
+ // version.
+ std::string message(failed_build.status().message());
+ EXPECT_THAT(message,
+ MatchesRegex("^L1 sensitivity must be finite and positive.*"));
+}
+
+TEST(NumericalMechanismsTest, LaplaceBuilderFailsL1SensitivityNegative) {
+ LaplaceMechanism::Builder test_builder;
+ auto failed_build = test_builder.SetL1Sensitivity(-1).SetEpsilon(1).Build();
+ EXPECT_THAT(failed_build.status().code(),
+ Eq(absl::StatusCode::kInvalidArgument));
+ // Convert message to std::string so that the matcher works in the open source
+ // version.
+ std::string message(failed_build.status().message());
+ EXPECT_THAT(message,
+ MatchesRegex("^L1 sensitivity must be finite and positive.*"));
+}
+
+TEST(NumericalMechanismsTest, LaplaceBuilderFailsL1SensitivityNan) {
+ LaplaceMechanism::Builder test_builder;
+ auto failed_build = test_builder.SetL1Sensitivity(NAN).SetEpsilon(1).Build();
+ EXPECT_THAT(failed_build.status().code(),
+ Eq(absl::StatusCode::kInvalidArgument));
+ // Convert message to std::string so that the matcher works in the open source
+ // version.
+ std::string message(failed_build.status().message());
+ EXPECT_THAT(message,
+ MatchesRegex("^L1 sensitivity must be a valid numeric value..*"));
+}
+
+TEST(NumericalMechanismsTest, LaplaceBuilderFailsL1SensitivityInfinity) {
+ LaplaceMechanism::Builder test_builder;
+ auto failed_build =
+ test_builder.SetL1Sensitivity(INFINITY).SetEpsilon(1).Build();
+ EXPECT_THAT(failed_build.status().code(),
+ Eq(absl::StatusCode::kInvalidArgument));
+ // Convert message to std::string so that the matcher works in the open source
+ // version.
+ std::string message(failed_build.status().message());
+ EXPECT_THAT(message, MatchesRegex("^L1 sensitivity must be finite.*"));
+}
+
+TEST(NumericalMechanismsTest, LaplaceBuilderFailsL0SensitivityZero) {
+ LaplaceMechanism::Builder test_builder;
+ auto failed_build = test_builder.SetL0Sensitivity(0)
.SetLInfSensitivity(1)
.SetEpsilon(1)
.Build();
@@ -129,35 +178,8 @@
// Convert message to std::string so that the matcher works in the open source
// version.
std::string message(failed_build.status().message());
- EXPECT_THAT(message, MatchesRegex("^L0 sensitivity has to be finite.*"));
-}
-
-TEST(NumericalMechanismsTest, LaplaceBuilderFailsL0SensitivityInfinity) {
- LaplaceMechanism::Builder test_builder;
- auto failed_build = test_builder.SetL0Sensitivity(INFINITY)
- .SetLInfSensitivity(1)
- .SetEpsilon(1)
- .Build();
- EXPECT_THAT(failed_build.status().code(),
- Eq(absl::StatusCode::kInvalidArgument));
- // Convert message to std::string so that the matcher works in the open source
- // version.
- std::string message(failed_build.status().message());
- EXPECT_THAT(message, MatchesRegex("^L0 sensitivity has to be finite.*"));
-}
-
-TEST(NumericalMechanismsTest, LaplaceBuilderFailsLInfSensitivityNan) {
- LaplaceMechanism::Builder test_builder;
- auto failed_build = test_builder.SetL0Sensitivity(1)
- .SetLInfSensitivity(NAN)
- .SetEpsilon(1)
- .Build();
- EXPECT_THAT(failed_build.status().code(),
- Eq(absl::StatusCode::kInvalidArgument));
- // Convert message to std::string so that the matcher works in the open source
- // version.
- std::string message(failed_build.status().message());
- EXPECT_THAT(message, MatchesRegex("^LInf sensitivity has to be finite.*"));
+ EXPECT_THAT(message,
+ MatchesRegex("^L0 sensitivity must be finite and positive.*"));
}
TEST(NumericalMechanismsTest, LaplaceBuilderFailsL0SensitivityNegative) {
@@ -172,7 +194,22 @@
// version.
std::string message(failed_build.status().message());
EXPECT_THAT(message,
- MatchesRegex("^L0 sensitivity has to be positive but is.*"));
+ MatchesRegex("^L0 sensitivity must be finite and positive.*"));
+}
+
+TEST(NumericalMechanismsTest, LaplaceBuilderFailsL0SensitivityNan) {
+ LaplaceMechanism::Builder test_builder;
+ auto failed_build = test_builder.SetL0Sensitivity(NAN)
+ .SetLInfSensitivity(1)
+ .SetEpsilon(1)
+ .Build();
+ EXPECT_THAT(failed_build.status().code(),
+ Eq(absl::StatusCode::kInvalidArgument));
+ // Convert message to std::string so that the matcher works in the open source
+ // version.
+ std::string message(failed_build.status().message());
+ EXPECT_THAT(message,
+ MatchesRegex("^L0 sensitivity must be a valid numeric value.*"));
}
TEST(NumericalMechanismsTest, LaplaceBuilderFailsLInfSensitivityZero) {
@@ -186,8 +223,55 @@
// Convert message to std::string so that the matcher works in the open source
// version.
std::string message(failed_build.status().message());
- EXPECT_THAT(message,
- MatchesRegex("^LInf sensitivity has to be positive but is.*"));
+ EXPECT_THAT(
+ message,
+ MatchesRegex("^LInf sensitivity must be finite and positive, but is.*"));
+}
+
+TEST(NumericalMechanismsTest, LaplaceBuilderFailsLInfSensitivityNegative) {
+ LaplaceMechanism::Builder test_builder;
+ auto failed_build = test_builder.SetL0Sensitivity(1)
+ .SetLInfSensitivity(-1)
+ .SetEpsilon(1)
+ .Build();
+ EXPECT_THAT(failed_build.status().code(),
+ Eq(absl::StatusCode::kInvalidArgument));
+ // Convert message to std::string so that the matcher works in the open source
+ // version.
+ std::string message(failed_build.status().message());
+ EXPECT_THAT(
+ message,
+ MatchesRegex("^LInf sensitivity must be finite and positive, but is.*"));
+}
+
+TEST(NumericalMechanismsTest, LaplaceBuilderFailsLInfSensitivityNan) {
+ LaplaceMechanism::Builder test_builder;
+ auto failed_build = test_builder.SetL0Sensitivity(1)
+ .SetLInfSensitivity(NAN)
+ .SetEpsilon(1)
+ .Build();
+ EXPECT_THAT(failed_build.status().code(),
+ Eq(absl::StatusCode::kInvalidArgument));
+ // Convert message to std::string so that the matcher works in the open source
+ // version.
+ std::string message(failed_build.status().message());
+ EXPECT_THAT(
+ message,
+ MatchesRegex("^LInf sensitivity must be a valid numeric value.*"));
+}
+
+TEST(NumericalMechanismsTest, LaplaceBuilderFailsL0SensitivityInfinity) {
+ LaplaceMechanism::Builder test_builder;
+ auto failed_build = test_builder.SetL0Sensitivity(INFINITY)
+ .SetLInfSensitivity(1)
+ .SetEpsilon(1)
+ .Build();
+ EXPECT_THAT(failed_build.status().code(),
+ Eq(absl::StatusCode::kInvalidArgument));
+ // Convert message to std::string so that the matcher works in the open source
+ // version.
+ std::string message(failed_build.status().message());
+ EXPECT_THAT(message, MatchesRegex("^L0 sensitivity must be finite.*"));
}
TYPED_TEST(NumericalMechanismsTest, LaplaceBuilderSensitivityTooHigh) {
@@ -305,18 +389,20 @@
TEST(NumericalMechanismsTest, LaplaceConfidenceIntervalFailsForBudgetNan) {
LaplaceMechanism mechanism(1.0, 1.0);
auto failed_confidence_interval = mechanism.NoiseConfidenceInterval(0.5, NAN);
- EXPECT_THAT(failed_confidence_interval,
- StatusIs(absl::StatusCode::kInvalidArgument,
- HasSubstr("privacy_budget has to be in")));
+ EXPECT_THAT(
+ failed_confidence_interval,
+ StatusIs(absl::StatusCode::kInvalidArgument,
+ HasSubstr("Privacy budget must be a valid numeric value")));
}
TEST(NumericalMechanismsTest,
LaplaceConfidenceIntervalFailsForConfidenceLevelNan) {
LaplaceMechanism mechanism(1.0, 1.0);
auto failed_confidence_interval = mechanism.NoiseConfidenceInterval(NAN, 1.0);
- EXPECT_THAT(failed_confidence_interval,
- StatusIs(absl::StatusCode::kInvalidArgument,
- HasSubstr("Confidence level has to be in")));
+ EXPECT_THAT(
+ failed_confidence_interval,
+ StatusIs(absl::StatusCode::kInvalidArgument,
+ HasSubstr("Confidence level must be a valid numeric value")));
}
TYPED_TEST(NumericalMechanismsTest, LaplaceBuilderClone) {
@@ -441,7 +527,7 @@
// Convert message to std::string so that the matcher works in the open source
// version.
std::string message(failed_build.status().message());
- EXPECT_THAT(message, MatchesRegex("^Delta has to be set.*"));
+ EXPECT_THAT(message, MatchesRegex("^Delta must be set.*"));
}
TEST(NumericalMechanismsTest, GaussianBuilderFailsDeltaNan) {
@@ -453,7 +539,7 @@
// Convert message to std::string so that the matcher works in the open source
// version.
std::string message(failed_build.status().message());
- EXPECT_THAT(message, MatchesRegex("^Delta has to be finite.*"));
+ EXPECT_THAT(message, MatchesRegex("^Delta must be a valid numeric value.*"));
}
TEST(NumericalMechanismsTest, GaussianBuilderFailsDeltaNegative) {
@@ -465,7 +551,8 @@
// Convert message to std::string so that the matcher works in the open source
// version.
std::string message(failed_build.status().message());
- EXPECT_THAT(message, MatchesRegex("^Delta has to be in the interval.*"));
+ EXPECT_THAT(message,
+ MatchesRegex("^Delta must be in the exclusive interval.*"));
}
TEST(NumericalMechanismsTest, GaussianBuilderFailsDeltaOne) {
@@ -477,7 +564,8 @@
// Convert message to std::string so that the matcher works in the open source
// version.
std::string message(failed_build.status().message());
- EXPECT_THAT(message, MatchesRegex("^Delta has to be in the interval.*"));
+ EXPECT_THAT(message,
+ MatchesRegex("^Delta must be in the exclusive interval.*"));
}
TEST(NumericalMechanismsTest, GaussianBuilderFailsDeltaZero) {
@@ -489,7 +577,8 @@
// Convert message to std::string so that the matcher works in the open source
// version.
std::string message(failed_build.status().message());
- EXPECT_THAT(message, MatchesRegex("^Delta has to be in the interval.*"));
+ EXPECT_THAT(message,
+ MatchesRegex("^Delta must be in the exclusive interval.*"));
}
TEST(NumericalMechanismsTest, GaussianBuilderFailsL0SensitivityNan) {
@@ -504,7 +593,8 @@
// Convert message to std::string so that the matcher works in the open source
// version.
std::string message(failed_build.status().message());
- EXPECT_THAT(message, MatchesRegex("^L0 sensitivity has to be finite.*"));
+ EXPECT_THAT(message,
+ MatchesRegex("^L0 sensitivity must be a valid numeric value.*"));
}
TEST(NumericalMechanismsTest, GaussianBuilderFailsLInfSensitivityInfinity) {
@@ -519,7 +609,7 @@
// Convert message to std::string so that the matcher works in the open source
// version.
std::string message(failed_build.status().message());
- EXPECT_THAT(message, MatchesRegex("^LInf sensitivity has to be finite.*"));
+ EXPECT_THAT(message, MatchesRegex("^LInf sensitivity must be finite.*"));
}
TEST(NumericalMechanismsTest, GaussianBuilderFailsL2SensitivityNan) {
@@ -531,7 +621,8 @@
// Convert message to std::string so that the matcher works in the open source
// version.
std::string message(failed_build.status().message());
- EXPECT_THAT(message, MatchesRegex("^L2 sensitivity has to be finite.*"));
+ EXPECT_THAT(message,
+ MatchesRegex("^L2 sensitivity must be a valid numeric value.*"));
}
TEST(NumericalMechanismsTest, GaussianBuilderFailsCalculatedL2SensitivityZero) {
@@ -551,7 +642,7 @@
EXPECT_THAT(
message,
MatchesRegex(
- "^The calculated L2 sensitivity has to be positive and finite.*"));
+ "^The calculated L2 sensitivity must be positive and finite.*"));
}
TEST(NumericalMechanismsTest, GaussianMechanismAddsNoise) {
diff --git a/cc/algorithms/order-statistics.h b/cc/algorithms/order-statistics.h
index b1b5a0d..60a9eeb 100644
--- a/cc/algorithms/order-statistics.h
+++ b/cc/algorithms/order-statistics.h
@@ -180,10 +180,8 @@
base::StatusOr<std::unique_ptr<Percentile<T>>> BuildBoundedAlgorithm()
override {
RETURN_IF_ERROR(OrderBuilder::ConstructDependencies());
- if (percentile_ < 0 || percentile_ > 1) {
- return absl::InvalidArgumentError(
- "Percentile must be between 0 and 1.");
- }
+ RETURN_IF_ERROR(
+ ValidateIsInInclusiveInterval(percentile_, 0, 1, "Percentile"));
return absl::WrapUnique(
new Percentile(percentile_, AlgorithmBuilder::GetEpsilon().value(),
BoundedBuilder::GetLower().value(),
diff --git a/cc/algorithms/order-statistics_test.cc b/cc/algorithms/order-statistics_test.cc
index 7f548a9..f6a3eea 100644
--- a/cc/algorithms/order-statistics_test.cc
+++ b/cc/algorithms/order-statistics_test.cc
@@ -160,12 +160,16 @@
builder.SetLower(3).Build(),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("Lower bound cannot be greater than upper bound")));
- EXPECT_THAT(builder.SetLower(1).SetPercentile(-1).Build(),
- StatusIs(absl::StatusCode::kInvalidArgument,
- HasSubstr("Percentile must be between 0 and 1")));
- EXPECT_THAT(builder.SetPercentile(2).Build(),
- StatusIs(absl::StatusCode::kInvalidArgument,
- HasSubstr("Percentile must be between 0 and 1")));
+ EXPECT_THAT(
+ builder.SetLower(1).SetPercentile(-1).Build(),
+ StatusIs(
+ absl::StatusCode::kInvalidArgument,
+ HasSubstr("Percentile must be in the inclusive interval [0,1]")));
+ EXPECT_THAT(
+ builder.SetPercentile(2).Build(),
+ StatusIs(
+ absl::StatusCode::kInvalidArgument,
+ HasSubstr("Percentile must be in the inclusive interval [0,1]")));
}
TEST(OrderStatisticsTest, Median_DefaultBounds) {
diff --git a/cc/algorithms/partition-selection.h b/cc/algorithms/partition-selection.h
index f4748fb..b589de6 100644
--- a/cc/algorithms/partition-selection.h
+++ b/cc/algorithms/partition-selection.h
@@ -115,50 +115,22 @@
// Checks if epsilon is set and valid.
static absl::Status EpsilonIsSetAndValid(absl::optional<double> epsilon) {
- if (!epsilon.has_value()) {
- return absl::InvalidArgumentError("Epsilon has to be set.");
- }
- if (!std::isfinite(epsilon.value())) {
- return absl::InvalidArgumentError(
- absl::StrCat("Epsilon has to be finite, but is ", epsilon.value()));
- }
- if (epsilon.value() <= 0) {
- return absl::InvalidArgumentError(
- absl::StrCat("Epsilon has to be positive, but is ", epsilon.value()));
- }
+ RETURN_IF_ERROR(ValidateIsFiniteAndPositive(epsilon, "Epsilon"));
return absl::OkStatus();
}
// Checks if delta is set and valid.
static absl::Status DeltaIsSetAndValid(absl::optional<double> delta) {
- if (!delta.has_value()) {
- return absl::InvalidArgumentError("Delta has to be set.");
- }
- if (!std::isfinite(delta.value())) {
- return absl::InvalidArgumentError(
- absl::StrCat("Delta has to be finite, but is ", delta.value()));
- }
- if (delta.value() < 0 || delta.value() > 1) {
- return absl::InvalidArgumentError(absl::StrCat(
- "Delta has to be in the inclusive interval [0,1], but is ",
- delta.value()));
- }
+ RETURN_IF_ERROR(ValidateIsInInclusiveInterval(delta, 0, 1, "Delta"));
return absl::OkStatus();
}
// Checks if the max number of partitions contributed to is set and valid.
static absl::Status MaxPartitionsContributedIsSetAndValid(
absl::optional<int64_t> max_partitions_contributed) {
- if (!max_partitions_contributed.has_value()) {
- return absl::InvalidArgumentError(
- "Max number of partitions a user can contribute to has to be set.");
- }
- if (max_partitions_contributed.value() <= 0) {
- return absl::InvalidArgumentError(absl::StrCat(
- "Max number of partitions a user can contribute to has to be"
- " positive, but is ",
- max_partitions_contributed.value()));
- }
+ RETURN_IF_ERROR(ValidateIsPositive(
+ max_partitions_contributed,
+ "Max number of partitions a user can contribute to"));
return absl::OkStatus();
}
diff --git a/cc/algorithms/partition-selection_test.cc b/cc/algorithms/partition-selection_test.cc
index 6c71ae0..b7b80ae 100644
--- a/cc/algorithms/partition-selection_test.cc
+++ b/cc/algorithms/partition-selection_test.cc
@@ -53,10 +53,10 @@
EXPECT_THAT(failed_build.status().code(),
Eq(absl::StatusCode::kInvalidArgument));
std::string message(std::string(failed_build.status().message()));
- EXPECT_THAT(message, MatchesRegex("^Epsilon has to be set.*"));
+ EXPECT_THAT(message, MatchesRegex("^Epsilon must be set.*"));
}
-TEST(PartitionSelectionTest, PreaggPartitionSelectionNotFiniteEpsilon) {
+TEST(PartitionSelectionTest, PreaggPartitionSelectionNanEpsilon) {
PreaggPartitionSelection::Builder test_builder;
auto failed_build = test_builder.SetEpsilon(NAN)
.SetDelta(0.3)
@@ -65,7 +65,21 @@
EXPECT_THAT(failed_build.status().code(),
Eq(absl::StatusCode::kInvalidArgument));
std::string message(std::string(failed_build.status().message()));
- EXPECT_THAT(message, MatchesRegex("^Epsilon has to be finite.*"));
+ EXPECT_THAT(message,
+ MatchesRegex("^Epsilon must be a valid numeric value.*"));
+}
+
+TEST(PartitionSelectionTest, PreaggPartitionSelectionNotFiniteEpsilon) {
+ PreaggPartitionSelection::Builder test_builder;
+ auto failed_build =
+ test_builder.SetEpsilon(std::numeric_limits<double>::infinity())
+ .SetDelta(0.3)
+ .SetMaxPartitionsContributed(4)
+ .Build();
+ EXPECT_THAT(failed_build.status().code(),
+ Eq(absl::StatusCode::kInvalidArgument));
+ std::string message(std::string(failed_build.status().message()));
+ EXPECT_THAT(message, MatchesRegex("^Epsilon must be finite.*"));
}
TEST(PartitionSelectionTest, PreaggPartitionSelectionNegativeEpsilon) {
@@ -77,7 +91,7 @@
EXPECT_THAT(failed_build.status().code(),
Eq(absl::StatusCode::kInvalidArgument));
std::string message(std::string(failed_build.status().message()));
- EXPECT_THAT(message, MatchesRegex("^Epsilon has to be positive.*"));
+ EXPECT_THAT(message, MatchesRegex("^Epsilon must be finite and positive.*"));
}
TEST(PartitionSelectionTest, PreaggPartitionSelectionUnsetDelta) {
@@ -87,10 +101,10 @@
EXPECT_THAT(failed_build.status().code(),
Eq(absl::StatusCode::kInvalidArgument));
std::string message(std::string(failed_build.status().message()));
- EXPECT_THAT(message, MatchesRegex("^Delta has to be set.*"));
+ EXPECT_THAT(message, MatchesRegex("^Delta must be set.*"));
}
-TEST(PartitionSelectionTest, PreaggPartitionSelectionNotFiniteDelta) {
+TEST(PartitionSelectionTest, PreaggPartitionSelectionNanDelta) {
PreaggPartitionSelection::Builder test_builder;
auto failed_build = test_builder.SetEpsilon(1.2)
.SetDelta(NAN)
@@ -99,7 +113,20 @@
EXPECT_THAT(failed_build.status().code(),
Eq(absl::StatusCode::kInvalidArgument));
std::string message(std::string(failed_build.status().message()));
- EXPECT_THAT(message, MatchesRegex("^Delta has to be finite.*"));
+ EXPECT_THAT(message, MatchesRegex("^Delta must be a valid numeric value.*"));
+}
+
+TEST(PartitionSelectionTest, PreaggPartitionSelectionNotFiniteDelta) {
+ PreaggPartitionSelection::Builder test_builder;
+ auto failed_build = test_builder.SetEpsilon(1.2)
+ .SetDelta(std::numeric_limits<double>::infinity())
+ .SetMaxPartitionsContributed(3)
+ .Build();
+ EXPECT_THAT(failed_build.status().code(),
+ Eq(absl::StatusCode::kInvalidArgument));
+ std::string message(std::string(failed_build.status().message()));
+ EXPECT_THAT(message,
+ MatchesRegex("^Delta must be in the inclusive interval.*"));
}
TEST(PartitionSelectionTest, PreaggPartitionSelectionInvalidDelta) {
@@ -112,7 +139,7 @@
Eq(absl::StatusCode::kInvalidArgument));
std::string message(std::string(failed_build.status().message()));
EXPECT_THAT(message,
- MatchesRegex("^Delta has to be in the inclusive interval.*"));
+ MatchesRegex("^Delta must be in the inclusive interval.*"));
}
TEST(PartitionSelectionTest,
@@ -123,7 +150,7 @@
Eq(absl::StatusCode::kInvalidArgument));
std::string message(std::string(failed_build.status().message()));
EXPECT_THAT(message, MatchesRegex("^Max number of partitions a user can"
- " contribute to has to be set.*"));
+ " contribute to must be set.*"));
}
TEST(PartitionSelectionTest,
@@ -137,7 +164,21 @@
Eq(absl::StatusCode::kInvalidArgument));
std::string message(std::string(failed_build.status().message()));
EXPECT_THAT(message, MatchesRegex("^Max number of partitions a user can"
- " contribute to has to be positive.*"));
+ " contribute to must be positive.*"));
+}
+
+TEST(PartitionSelectionTest,
+ PreaggPartitionSelectionZeroMaxPartitionsContributed) {
+ PreaggPartitionSelection::Builder test_builder;
+ auto failed_build = test_builder.SetEpsilon(0.1)
+ .SetDelta(0.2)
+ .SetMaxPartitionsContributed(0)
+ .Build();
+ EXPECT_THAT(failed_build.status().code(),
+ Eq(absl::StatusCode::kInvalidArgument));
+ std::string message(std::string(failed_build.status().message()));
+ EXPECT_THAT(message, MatchesRegex("^Max number of partitions a user can"
+ " contribute to must be positive.*"));
}
// We expect the probability of keeping a partition with one user
@@ -310,7 +351,7 @@
Eq(absl::StatusCode::kInvalidArgument));
std::string message(std::string(failed_build.status().message()));
EXPECT_THAT(message, MatchesRegex("^Max number of partitions a user can"
- " contribute to has to be set.*"));
+ " contribute to must be set.*"));
}
TEST(PartitionSelectionTest,
@@ -327,7 +368,24 @@
Eq(absl::StatusCode::kInvalidArgument));
std::string message(std::string(failed_build.status().message()));
EXPECT_THAT(message, MatchesRegex("^Max number of partitions a user can"
- " contribute to has to be positive.*"));
+ " contribute to must be positive.*"));
+}
+
+TEST(PartitionSelectionTest,
+ LaplacePartitionSelectionZeroMaxPartitionsContributed) {
+ LaplacePartitionSelection::Builder test_builder;
+ auto failed_build =
+ test_builder
+ .SetLaplaceMechanism(absl::make_unique<LaplaceMechanism::Builder>())
+ .SetDelta(0.1)
+ .SetEpsilon(2)
+ .SetMaxPartitionsContributed(0)
+ .Build();
+ EXPECT_THAT(failed_build.status().code(),
+ Eq(absl::StatusCode::kInvalidArgument));
+ std::string message(std::string(failed_build.status().message()));
+ EXPECT_THAT(message, MatchesRegex("^Max number of partitions a user can"
+ " contribute to must be positive.*"));
}
TEST(PartitionSelectionTest, LaplacePartitionSelectionUnsetEpsilon) {
@@ -341,7 +399,7 @@
EXPECT_THAT(failed_build.status().code(),
Eq(absl::StatusCode::kInvalidArgument));
std::string message(std::string(failed_build.status().message()));
- EXPECT_THAT(message, MatchesRegex("^Epsilon has to be set.*"));
+ EXPECT_THAT(message, MatchesRegex("^Epsilon must be set.*"));
}
TEST(PartitionSelectionTest, LaplacePartitionSelectionUnsetDelta) {
@@ -355,10 +413,10 @@
EXPECT_THAT(failed_build.status().code(),
Eq(absl::StatusCode::kInvalidArgument));
std::string message(std::string(failed_build.status().message()));
- EXPECT_THAT(message, MatchesRegex("^Delta has to be set.*"));
+ EXPECT_THAT(message, MatchesRegex("^Delta must be set.*"));
}
-TEST(PartitionSelectionTest, LaplacePartitionSelectionNotFiniteDelta) {
+TEST(PartitionSelectionTest, LaplacePartitionSelectionNanDelta) {
LaplacePartitionSelection::Builder test_builder;
auto failed_build =
test_builder
@@ -370,10 +428,26 @@
EXPECT_THAT(failed_build.status().code(),
Eq(absl::StatusCode::kInvalidArgument));
std::string message(std::string(failed_build.status().message()));
- EXPECT_THAT(message, MatchesRegex("^Delta has to be finite.*"));
+ EXPECT_THAT(message, MatchesRegex("^Delta must be a valid numeric value.*"));
}
-TEST(PartitionSelectionTest, LaplacePartitionSelectionInvalidDelta) {
+TEST(PartitionSelectionTest, LaplacePartitionSelectionNotFiniteDelta) {
+ LaplacePartitionSelection::Builder test_builder;
+ auto failed_build =
+ test_builder
+ .SetLaplaceMechanism(absl::make_unique<LaplaceMechanism::Builder>())
+ .SetEpsilon(0.1)
+ .SetDelta(std::numeric_limits<double>::infinity())
+ .SetMaxPartitionsContributed(2)
+ .Build();
+ EXPECT_THAT(failed_build.status().code(),
+ Eq(absl::StatusCode::kInvalidArgument));
+ std::string message(std::string(failed_build.status().message()));
+ EXPECT_THAT(message,
+ MatchesRegex("^Delta must be in the inclusive interval.*"));
+}
+
+TEST(PartitionSelectionTest, LaplacePartitionSelectionInvalidPositiveDelta) {
LaplacePartitionSelection::Builder test_builder;
auto failed_build =
test_builder
@@ -386,7 +460,23 @@
Eq(absl::StatusCode::kInvalidArgument));
std::string message(std::string(failed_build.status().message()));
EXPECT_THAT(message,
- MatchesRegex("^Delta has to be in the inclusive interval.*"));
+ MatchesRegex("^Delta must be in the inclusive interval.*"));
+}
+
+TEST(PartitionSelectionTest, LaplacePartitionSelectionInvalidNegativeDelta) {
+ LaplacePartitionSelection::Builder test_builder;
+ auto failed_build =
+ test_builder
+ .SetLaplaceMechanism(absl::make_unique<LaplaceMechanism::Builder>())
+ .SetEpsilon(0.1)
+ .SetDelta(-0.1)
+ .SetMaxPartitionsContributed(2)
+ .Build();
+ EXPECT_THAT(failed_build.status().code(),
+ Eq(absl::StatusCode::kInvalidArgument));
+ std::string message(std::string(failed_build.status().message()));
+ EXPECT_THAT(message,
+ MatchesRegex("^Delta must be in the inclusive interval.*"));
}
// We expect the probability of keeping a partition with one user
diff --git a/cc/algorithms/util.cc b/cc/algorithms/util.cc
index 0feed45..0ec137c 100644
--- a/cc/algorithms/util.cc
+++ b/cc/algorithms/util.cc
@@ -19,6 +19,7 @@
#include <cmath>
#include <vector>
+#include "absl/status/status.h"
#include "base/statusor.h"
#include "base/canonical_errors.h"
@@ -115,26 +116,201 @@
return 0;
}
-base::StatusOr<double> GetValueIfSetAndFinite(absl::optional<double> opt,
- absl::string_view name) {
+absl::Status ValidateIsSet(absl::optional<double> opt, absl::string_view name,
+ absl::StatusCode error_code) {
if (!opt.has_value()) {
- return absl::InvalidArgumentError(absl::StrCat(name, " has to be set."));
+ return absl::InvalidArgumentError(absl::StrCat(name, " must be set."));
}
- if (!std::isfinite(opt.value())) {
- return absl::InvalidArgumentError(
- absl::StrCat(name, " has to be finite but is ", opt.value()));
+ double d = opt.value();
+ if (isnan(d)) {
+ return absl::Status(
+ error_code,
+ absl::StrCat(name, " must be a valid numeric value, but is ", d, "."));
}
- return opt.value();
+ return absl::OkStatus();
}
-base::StatusOr<double> GetValueIfSetAndPositive(absl::optional<double> opt,
- absl::string_view name) {
- ASSIGN_OR_RETURN(double d, GetValueIfSetAndFinite(opt, name));
+absl::Status ValidateIsPositive(absl::optional<double> opt,
+ absl::string_view name,
+ absl::StatusCode error_code) {
+ RETURN_IF_ERROR(ValidateIsSet(opt, name, error_code));
+ double d = opt.value();
if (d <= 0) {
- return absl::InvalidArgumentError(
- absl::StrCat(name, " has to be positive but is ", d));
+ return absl::Status(
+ error_code, absl::StrCat(name, " must be positive, but is ", d, "."));
}
- return d;
+ return absl::OkStatus();
+}
+
+absl::Status ValidateIsNonNegative(absl::optional<double> opt,
+ absl::string_view name,
+ absl::StatusCode error_code) {
+ RETURN_IF_ERROR(ValidateIsSet(opt, name, error_code));
+ double d = opt.value();
+ if (d < 0) {
+ return absl::Status(
+ error_code,
+ absl::StrCat(name, " must be non-negative, but is ", d, "."));
+ }
+ return absl::OkStatus();
+}
+
+absl::Status ValidateIsFinite(absl::optional<double> opt,
+ absl::string_view name,
+ absl::StatusCode error_code) {
+ RETURN_IF_ERROR(ValidateIsSet(opt, name, error_code));
+ double d = opt.value();
+ if (!std::isfinite(d)) {
+ return absl::Status(error_code,
+ absl::StrCat(name, " must be finite, but is ", d, "."));
+ }
+ return absl::OkStatus();
+}
+
+absl::Status ValidateIsFiniteAndPositive(absl::optional<double> opt,
+ absl::string_view name,
+ absl::StatusCode error_code) {
+ RETURN_IF_ERROR(ValidateIsSet(opt, name, error_code));
+ double d = opt.value();
+ if (d <= 0 || !std::isfinite(d)) {
+ return absl::Status(
+ error_code,
+ absl::StrCat(name, " must be finite and positive, but is ", d, "."));
+ }
+ return absl::OkStatus();
+}
+
+absl::Status ValidateIsFiniteAndNonNegative(absl::optional<double> opt,
+ absl::string_view name,
+ absl::StatusCode error_code) {
+ RETURN_IF_ERROR(ValidateIsSet(opt, name, error_code));
+ double d = opt.value();
+ if (d < 0 || !std::isfinite(d)) {
+ return absl::Status(
+ error_code,
+ absl::StrCat(name, " must be finite and non-negative, but is ", d,
+ "."));
+ }
+ return absl::OkStatus();
+}
+
+absl::Status ValidateIsInInclusiveInterval(absl::optional<double> opt,
+ double lower_bound,
+ double upper_bound,
+ absl::string_view name,
+ absl::StatusCode error_code) {
+ return ValidateIsInInterval(opt, lower_bound, upper_bound, true, true, name,
+ error_code);
+}
+
+absl::Status ValidateIsInExclusiveInterval(absl::optional<double> opt,
+ double lower_bound,
+ double upper_bound,
+ absl::string_view name,
+ absl::StatusCode error_code) {
+ return ValidateIsInInterval(opt, lower_bound, upper_bound, false, false, name,
+ error_code);
+}
+
+absl::Status ValidateIsLesserThan(absl::optional<double> opt,
+ double upper_bound, absl::string_view name,
+ absl::StatusCode error_code) {
+ double lower_bound = -std::numeric_limits<double>::infinity();
+ bool include_lower = lower_bound != upper_bound;
+
+ absl::Status result = ValidateIsInInterval(
+ opt, lower_bound, upper_bound, include_lower, false, name, error_code);
+ if (result.ok()) {
+ return absl::OkStatus();
+ } else {
+ return absl::Status(error_code,
+ absl::StrCat(name, " must be lesser than ", upper_bound,
+ ", but is ", opt.value(), "."));
+ }
+}
+
+absl::Status ValidateIsLesserThanOrEqualTo(absl::optional<double> opt,
+ double upper_bound,
+ absl::string_view name,
+ absl::StatusCode error_code) {
+ absl::Status result =
+ ValidateIsInInterval(opt, -std::numeric_limits<double>::infinity(),
+ upper_bound, true, true, name, error_code);
+ if (result.ok()) {
+ return absl::OkStatus();
+ } else {
+ return absl::Status(
+ error_code, absl::StrCat(name, " must be lesser than or equal to ",
+ upper_bound, ", but is ", opt.value(), "."));
+ }
+}
+
+absl::Status ValidateIsGreaterThan(absl::optional<double> opt,
+ double lower_bound, absl::string_view name,
+ absl::StatusCode error_code) {
+ double upper_bound = std::numeric_limits<double>::infinity();
+ bool include_upper = lower_bound != upper_bound;
+
+ absl::Status result = ValidateIsInInterval(
+ opt, lower_bound, upper_bound, false, include_upper, name, error_code);
+ if (result.ok()) {
+ return absl::OkStatus();
+ } else {
+ return absl::Status(
+ error_code, absl::StrCat(name, " must be greater than ", lower_bound,
+ ", but is ", opt.value(), "."));
+ }
+}
+
+absl::Status ValidateIsGreaterThanOrEqualTo(absl::optional<double> opt,
+ double lower_bound,
+ absl::string_view name,
+ absl::StatusCode error_code) {
+ absl::Status result = ValidateIsInInterval(
+ opt, lower_bound, std::numeric_limits<double>::infinity(), true, true,
+ name, error_code);
+ if (result.ok()) {
+ return absl::OkStatus();
+ } else {
+ return absl::Status(
+ error_code, absl::StrCat(name, " must be greater than or equal to ",
+ lower_bound, ", but is ", opt.value(), "."));
+ }
+}
+
+absl::Status ValidateIsInInterval(absl::optional<double> opt,
+ double lower_bound, double upper_bound,
+ bool include_lower, bool include_upper,
+ absl::string_view name,
+ absl::StatusCode error_code) {
+ RETURN_IF_ERROR(ValidateIsSet(opt, name, error_code));
+ double d = opt.value();
+
+ if (lower_bound == upper_bound && upper_bound == d &&
+ (include_lower || include_upper)) {
+ return absl::OkStatus();
+ }
+ bool d_is_outside_lower_bound =
+ include_lower ? d < lower_bound : d <= lower_bound;
+ bool d_is_outside_upper_bound =
+ include_upper ? d > upper_bound : d >= upper_bound;
+ if (d_is_outside_lower_bound || d_is_outside_upper_bound) {
+ std::string left_bracket = include_lower ? "[" : "(";
+ std::string right_bracket = include_upper ? "]" : ")";
+ std::string inclusivity = " ";
+ if (include_lower && include_upper) {
+ inclusivity = " inclusive ";
+ } else if (!include_lower && !include_upper) {
+ inclusivity = " exclusive ";
+ }
+
+ return absl::Status(
+ error_code,
+ absl::StrCat(name, " must be in the", inclusivity, "interval ",
+ left_bracket, lower_bound, ",", upper_bound, right_bracket,
+ ", but is ", d, "."));
+ }
+ return absl::OkStatus();
}
} // namespace differential_privacy
diff --git a/cc/algorithms/util.h b/cc/algorithms/util.h
index 924c8fd..d219d6b 100644
--- a/cc/algorithms/util.h
+++ b/cc/algorithms/util.h
@@ -19,7 +19,6 @@
#include <algorithm>
#include <cmath>
-#include <limits>
#include <numeric>
#include <string>
#include <type_traits>
@@ -148,44 +147,6 @@
return true;
}
-// When T is an integral type, return true and assign the multiplication result
-// if the multiplication will not overflow. Otherwise, assign the numeric limit
-// to result and return false.
-template <typename T, std::enable_if_t<std::is_integral<T>::value>* = nullptr>
-inline bool SafeMultiply(T lhs, T rhs, T* result) {
- if (lhs > 0) {
- if (rhs > 0) {
- T safe_distance = std::numeric_limits<T>::max() / lhs;
- if (safe_distance < rhs) {
- *result = std::numeric_limits<T>::max();
- return false;
- }
- } else if (rhs < 0) {
- T safe_distance = std::numeric_limits<T>::lowest() / lhs;
- if (safe_distance > rhs) {
- *result = std::numeric_limits<T>::lowest();
- return false;
- }
- }
- } else if (lhs < 0) {
- if (rhs < 0) {
- T safe_distance = std::numeric_limits<T>::max() / lhs;
- if (safe_distance > rhs) {
- *result = std::numeric_limits<T>::max();
- return false;
- }
- } else if (rhs > 0) {
- T safe_distance = std::numeric_limits<T>::lowest() / rhs;
- if (safe_distance > lhs) {
- *result = std::numeric_limits<T>::lowest();
- return false;
- }
- }
- }
- *result = lhs * rhs;
- return true;
-}
-
// When T is a floating-point type, perform a simple multiplication, since
// floating-point types don't have the same overflow issues as integral types.
template <typename T,
@@ -333,17 +294,106 @@
return absl::StrCat("[", absl::StrJoin(v, ", "), "]");
}
-// Returns the value of optional `opt` if it is set and finite. Will return
-// an InvalidArgumentError otherwise that includes `name` in the error
-// message.
-base::StatusOr<double> GetValueIfSetAndFinite(absl::optional<double> opt,
- absl::string_view name);
+// The functions below provide a common and consistent way for validating
+// arguments and formatting error messages.
-// Returns the value of optional `opt` if it is set, finite, and positive.
-// Will return an InvalidArgumentError otherwise that includes `name` in the
+// Returns absl::OkStatus() if the value of optional `opt` if it is set.
+// Otherwise, will return an `error_code` error that includes `name` in the
// error message.
-base::StatusOr<double> GetValueIfSetAndPositive(absl::optional<double> opt,
- absl::string_view name);
+absl::Status ValidateIsSet(
+ absl::optional<double> opt, absl::string_view name,
+ absl::StatusCode error_code = absl::StatusCode::kInvalidArgument);
+
+// Returns absl::OkStatus() if the value of optional `opt` if it is set and
+// positive. Otherwise, will return an `error_code` error status that includes
+// `name` in the error message.
+absl::Status ValidateIsPositive(
+ absl::optional<double> opt, absl::string_view name,
+ absl::StatusCode error_code = absl::StatusCode::kInvalidArgument);
+
+// Returns absl::OkStatus() if the value of optional `opt` if it is set and
+// non-negative. Otherwise, will return an `error_code` error status that
+// includes `name` in the error message.
+absl::Status ValidateIsNonNegative(
+ absl::optional<double> opt, absl::string_view name,
+ absl::StatusCode error_code = absl::StatusCode::kInvalidArgument);
+
+// Returns absl::OkStatus() if the value of optional `opt` if it is set and
+// finite. Otherwise, will return an `error_code` error status that includes
+// `name` in the error message.
+absl::Status ValidateIsFinite(
+ absl::optional<double> opt, absl::string_view name,
+ absl::StatusCode error_code = absl::StatusCode::kInvalidArgument);
+
+// Returns absl::OkStatus() if the value of optional `opt` if it is set, finite,
+// and positive. Otherwise, will return an `error_code` error status that
+// includes `name` in the error message.
+absl::Status ValidateIsFiniteAndPositive(
+ absl::optional<double> opt, absl::string_view name,
+ absl::StatusCode error_code = absl::StatusCode::kInvalidArgument);
+
+// Returns absl::OkStatus() if the value of optional `opt` if it is set, finite,
+// and non-negative. Otherwise, will return an `error_code` error status that
+// includes `name` in the error message.
+absl::Status ValidateIsFiniteAndNonNegative(
+ absl::optional<double> opt, absl::string_view name,
+ absl::StatusCode error_code = absl::StatusCode::kInvalidArgument);
+
+// Returns absl::OkStatus() if the value of optional `opt` if it is set and
+// within the inclusive (i.e., closed) interval [`lower_bound`, `upper_bound`].
+// Otherwise, will return an `error_code` error status that includes `name` in
+// the error message.
+absl::Status ValidateIsInInclusiveInterval(
+ absl::optional<double> opt, double lower_bound, double upper_bound,
+ absl::string_view name,
+ absl::StatusCode error_code = absl::StatusCode::kInvalidArgument);
+
+// Returns absl::OkStatus() if the value of optional `opt` if it is set and
+// within the exclusive (i.e., open) interval (`lower_bound`, `upper_bound`).
+// Otherwise, will return an `error_code` error status that includes `name` in
+// the error message.
+absl::Status ValidateIsInExclusiveInterval(
+ absl::optional<double> opt, double lower_bound, double upper_bound,
+ absl::string_view name,
+ absl::StatusCode error_code = absl::StatusCode::kInvalidArgument);
+
+// Returns absl::OkStatus() if the value of optional `opt` if it is set and
+// strictly lesser than `upper_bound`. Otherwise, will return an `error_code`
+// error status that includes `name` in the error message.
+absl::Status ValidateIsLesserThan(
+ absl::optional<double> opt, double upper_bound, absl::string_view name,
+ absl::StatusCode error_code = absl::StatusCode::kInvalidArgument);
+
+// Returns absl::OkStatus() if the value of optional `opt` if it is set and
+// lesser than or equal to `upper_bound`. Otherwise, will return an `error_code`
+// error status that includes `name` in the error message.
+absl::Status ValidateIsLesserThanOrEqualTo(
+ absl::optional<double> opt, double upper_bound, absl::string_view name,
+ absl::StatusCode error_code = absl::StatusCode::kInvalidArgument);
+
+// Returns absl::OkStatus() if the value of optional `opt` if it is set and
+// strictly greater than `lower_bound`. Otherwise, will return an `error_code`
+// error status that includes `name` in the error message.
+absl::Status ValidateIsGreaterThan(
+ absl::optional<double> opt, double lower_bound, absl::string_view name,
+ absl::StatusCode error_code = absl::StatusCode::kInvalidArgument);
+
+// Returns absl::OkStatus() if the value of optional `opt` if it is set and
+// greater than or equal to `upper_bound`. Otherwise, will return an
+// `error_code` error status that includes `name` in the error message.
+absl::Status ValidateIsGreaterThanOrEqualTo(
+ absl::optional<double> opt, double lower_bound, absl::string_view name,
+ absl::StatusCode error_code = absl::StatusCode::kInvalidArgument);
+
+// Returns absl::OkStatus() if the value of optional `opt` if it is set and
+// within the interval between `lower_bound` and `upper_bound`, including
+// `lower_bound` and/or `upper_bound` if `include_lower` or `include_upper` are
+// true, respectively. Otherwise, will return an `error_code` error status that
+// includes `name` in the error message.
+absl::Status ValidateIsInInterval(
+ absl::optional<double> opt, double lower_bound, double upper_bound,
+ bool include_lower, bool include_upper, absl::string_view name,
+ absl::StatusCode error_code = absl::StatusCode::kInvalidArgument);
} // namespace differential_privacy
diff --git a/cc/algorithms/util_test.cc b/cc/algorithms/util_test.cc
index 13292fd..94a55b1 100644
--- a/cc/algorithms/util_test.cc
+++ b/cc/algorithms/util_test.cc
@@ -16,8 +16,8 @@
#include "algorithms/util.h"
-#include <limits>
-
+#include "base/testing/status_matchers.h"
+#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "absl/status/status.h"
#include "algorithms/distributions.h"
@@ -26,6 +26,9 @@
namespace differential_privacy {
namespace {
+using ::testing::HasSubstr;
+using ::differential_privacy::base::testing::StatusIs;
+
const char kSeedString[] = "ABCDEFGHIJKLMNOP";
constexpr int64_t kStatsSize = 50000;
constexpr double kTolerance = 1e-5;
@@ -256,95 +259,6 @@
EXPECT_EQ(double_result, 0);
}
-TEST(SafeOperationsTest, SafeMultiplyInt) {
- int64_t int_result;
-
- EXPECT_TRUE(SafeMultiply<int64_t>(1, 1, &int_result));
- EXPECT_EQ(int_result, 1);
- EXPECT_TRUE(SafeMultiply<int64_t>(-1, 1, &int_result));
- EXPECT_EQ(int_result, -1);
- EXPECT_TRUE(SafeMultiply<int64_t>(1, -1, &int_result));
- EXPECT_EQ(int_result, -1);
- EXPECT_TRUE(SafeMultiply<int64_t>(-1, -1, &int_result));
- EXPECT_EQ(int_result, 1);
- EXPECT_TRUE(SafeMultiply<int64_t>(10, -20, &int_result));
- EXPECT_EQ(int_result, -200);
-
- EXPECT_FALSE(SafeMultiply<int64_t>(std::numeric_limits<int64_t>::max(),
- std::numeric_limits<int64_t>::lowest(),
- &int_result));
- EXPECT_EQ(int_result, std::numeric_limits<int64_t>::lowest());
- EXPECT_FALSE(SafeMultiply<int64_t>(std::numeric_limits<int64_t>::lowest(),
- std::numeric_limits<int64_t>::max(),
- &int_result));
- EXPECT_EQ(int_result, std::numeric_limits<int64_t>::lowest());
-
- EXPECT_FALSE(
- SafeMultiply<int64_t>(std::numeric_limits<int64_t>::max(), 2, &int_result));
- EXPECT_EQ(int_result, std::numeric_limits<int64_t>::max());
- EXPECT_FALSE(SafeMultiply<int64_t>(std::numeric_limits<int64_t>::lowest(), -2,
- &int_result));
- EXPECT_EQ(int_result, std::numeric_limits<int64_t>::max());
-
- EXPECT_FALSE(
- SafeMultiply<int64_t>(std::numeric_limits<int64_t>::max(), -2, &int_result));
- EXPECT_EQ(int_result, std::numeric_limits<int64_t>::lowest());
- EXPECT_FALSE(
- SafeMultiply<int64_t>(-2, std::numeric_limits<int64_t>::max(), &int_result));
- EXPECT_EQ(int_result, std::numeric_limits<int64_t>::lowest());
-
- EXPECT_FALSE(SafeMultiply<int64_t>(std::numeric_limits<int64_t>::lowest(), 2,
- &int_result));
- EXPECT_EQ(int_result, std::numeric_limits<int64_t>::lowest());
- EXPECT_FALSE(SafeMultiply<int64_t>(2, std::numeric_limits<int64_t>::lowest(),
- &int_result));
- EXPECT_EQ(int_result, std::numeric_limits<int64_t>::lowest());
-
- EXPECT_TRUE(SafeMultiply<int64_t>(std::numeric_limits<int64_t>::lowest(), 0,
- &int_result));
- EXPECT_EQ(int_result, 0);
- EXPECT_TRUE(
- SafeMultiply<int64_t>(0, std::numeric_limits<int64_t>::max(), &int_result));
- EXPECT_EQ(int_result, 0);
-}
-
-TEST(SafeOperationsTest, SafeMultiplyDouble) {
- double double_result;
- EXPECT_TRUE(SafeMultiply<double>(1.0, 1.0, &double_result));
- EXPECT_DOUBLE_EQ(double_result, 1.0);
- EXPECT_TRUE(SafeMultiply<double>(-1.0, 1.0, &double_result));
- EXPECT_DOUBLE_EQ(double_result, -1.0);
- EXPECT_TRUE(SafeMultiply<double>(1.0, -1.0, &double_result));
- EXPECT_DOUBLE_EQ(double_result, -1.0);
- EXPECT_TRUE(SafeMultiply<double>(-1.0, -1.0, &double_result));
- EXPECT_DOUBLE_EQ(double_result, 1.0);
- EXPECT_TRUE(SafeMultiply<double>(10.0, -20.0, &double_result));
- EXPECT_DOUBLE_EQ(double_result, -200.0);
- EXPECT_TRUE(SafeMultiply<double>(std::numeric_limits<double>::max(),
- std::numeric_limits<double>::lowest(),
- &double_result));
- EXPECT_DOUBLE_EQ(double_result, std::numeric_limits<double>::max() *
- std::numeric_limits<double>::lowest());
- EXPECT_TRUE(SafeMultiply<double>(std::numeric_limits<double>::max(), 2.0,
- &double_result));
- EXPECT_DOUBLE_EQ(double_result, std::numeric_limits<double>::infinity());
- EXPECT_TRUE(SafeMultiply<double>(std::numeric_limits<double>::max(), -2.0,
- &double_result));
- EXPECT_DOUBLE_EQ(double_result, -std::numeric_limits<double>::infinity());
- EXPECT_TRUE(SafeMultiply<double>(std::numeric_limits<double>::lowest(), -2.0,
- &double_result));
- EXPECT_DOUBLE_EQ(double_result, std::numeric_limits<double>::infinity());
- EXPECT_TRUE(SafeMultiply<double>(std::numeric_limits<double>::lowest(), 2.0,
- &double_result));
- EXPECT_DOUBLE_EQ(double_result, -std::numeric_limits<double>::infinity());
- EXPECT_TRUE(SafeMultiply<double>(std::numeric_limits<double>::lowest(), 0.0,
- &double_result));
- EXPECT_EQ(double_result, 0);
- EXPECT_TRUE(SafeMultiply<double>(0.0, std::numeric_limits<double>::max(),
- &double_result));
- EXPECT_EQ(double_result, 0);
-}
-
TEST(SafeOperationsTest, SafeSquare) {
int64_t int_result;
EXPECT_TRUE(SafeSquare<int64_t>(-9, &int_result));
@@ -425,5 +339,516 @@
EXPECT_TRUE(std::isinf(floating_point));
}
+TEST(ValidateTest, IsSet) {
+ absl::optional<double> opt;
+ EXPECT_THAT(ValidateIsSet(opt, "Test value"),
+ StatusIs(absl::StatusCode::kInvalidArgument,
+ HasSubstr("Test value must be set.")));
+
+ opt = std::numeric_limits<double>::quiet_NaN();
+ EXPECT_THAT(ValidateIsSet(opt, "Test value"),
+ StatusIs(absl::StatusCode::kInvalidArgument,
+ HasSubstr("Test value must be a valid numeric value")));
+
+ std::vector<double> success_values = {
+ -std::numeric_limits<double>::infinity(),
+ std::numeric_limits<double>::lowest(),
+ -1,
+ 0,
+ std::numeric_limits<double>::min(),
+ 1,
+ std::numeric_limits<double>::max(),
+ std::numeric_limits<double>::infinity()};
+
+ for (double value : success_values) {
+ EXPECT_OK(ValidateIsSet(value, "Test value"));
+ }
+}
+
+TEST(ValidateTest, IsPositive) {
+ std::vector<double> success_values = {
+ std::numeric_limits<double>::min(), 1, std::numeric_limits<double>::max(),
+ std::numeric_limits<double>::infinity()};
+ std::vector<double> error_values = {-std::numeric_limits<double>::infinity(),
+ std::numeric_limits<double>::lowest(),
+ -10, -1, 0};
+
+ for (double value : success_values) {
+ EXPECT_OK(ValidateIsPositive(value, "Test value"));
+ }
+
+ for (double value : error_values) {
+ EXPECT_THAT(ValidateIsPositive(value, "Test value"),
+ StatusIs(absl::StatusCode::kInvalidArgument,
+ HasSubstr("Test value must be positive")));
+ }
+}
+
+TEST(ValidateTest, IsNonNegative) {
+ std::vector<double> success_values = {
+ 0, std::numeric_limits<double>::min(), 1,
+ std::numeric_limits<double>::max(),
+ std::numeric_limits<double>::infinity()};
+ std::vector<double> error_values = {-std::numeric_limits<double>::infinity(),
+ std::numeric_limits<double>::lowest(),
+ -10, -1};
+
+ for (double value : success_values) {
+ EXPECT_OK(ValidateIsNonNegative(value, "Test value"));
+ }
+
+ for (double value : error_values) {
+ EXPECT_THAT(ValidateIsNonNegative(value, "Test value"),
+ StatusIs(absl::StatusCode::kInvalidArgument,
+ HasSubstr("Test value must be non-negative")));
+ }
+}
+
+TEST(ValidateTest, IsFinite) {
+ std::vector<double> success_values = {std::numeric_limits<double>::lowest(),
+ -1,
+ 0,
+ std::numeric_limits<double>::min(),
+ 1,
+ std::numeric_limits<double>::max()};
+
+ std::vector<double> error_values = {-std::numeric_limits<double>::infinity(),
+ std::numeric_limits<double>::infinity()};
+
+ for (double value : success_values) {
+ EXPECT_OK(ValidateIsFinite(value, "Test value"));
+ }
+
+ for (double value : error_values) {
+ EXPECT_THAT(ValidateIsFinite(value, "Test value"),
+ StatusIs(absl::StatusCode::kInvalidArgument,
+ HasSubstr("Test value must be finite")));
+ }
+}
+
+TEST(ValidateTest, IsFiniteAndPositive) {
+ std::vector<double> success_values = {std::numeric_limits<double>::min(), 1,
+ std::numeric_limits<double>::max()};
+ std::vector<double> error_values = {-std::numeric_limits<double>::infinity(),
+ std::numeric_limits<double>::lowest(),
+ -10,
+ -1,
+ 0,
+ std::numeric_limits<double>::infinity()};
+
+ for (double value : success_values) {
+ EXPECT_OK(ValidateIsFiniteAndPositive(value, "Test value"));
+ }
+
+ for (double value : error_values) {
+ EXPECT_THAT(ValidateIsFiniteAndPositive(value, "Test value"),
+ StatusIs(absl::StatusCode::kInvalidArgument,
+ HasSubstr("Test value must be finite and positive")));
+ }
+}
+
+TEST(ValidateTest, IsFiniteAndNonNegative) {
+ std::vector<double> success_values = {0, std::numeric_limits<double>::min(),
+ 1, std::numeric_limits<double>::max()};
+ std::vector<double> error_values = {-std::numeric_limits<double>::infinity(),
+ std::numeric_limits<double>::lowest(),
+ -10, -1,
+ std::numeric_limits<double>::infinity()};
+
+ for (double value : success_values) {
+ EXPECT_OK(ValidateIsFiniteAndNonNegative(value, "Test value"));
+ }
+
+ for (double value : error_values) {
+ EXPECT_THAT(
+ ValidateIsFiniteAndNonNegative(value, "Test value"),
+ StatusIs(absl::StatusCode::kInvalidArgument,
+ HasSubstr("Test value must be finite and non-negative")));
+ }
+}
+
+TEST(ValidateTest, IsLesserThanOkStatus) {
+ struct LesserThanParams {
+ double value;
+ double upper_bound;
+ };
+
+ std::vector<LesserThanParams> success_params = {
+ {-std::numeric_limits<double>::infinity(),
+ std::numeric_limits<double>::lowest()},
+ {-1, 1},
+ {0, std::numeric_limits<double>::min()},
+ {std::numeric_limits<double>::max(),
+ std::numeric_limits<double>::infinity()},
+ };
+
+ for (LesserThanParams params : success_params) {
+ EXPECT_OK(
+ ValidateIsLesserThan(params.value, params.upper_bound, "Test value"));
+ }
+}
+
+TEST(ValidateTest, IsLesserThanError) {
+ struct LesserThanParams {
+ double value;
+ double upper_bound;
+ };
+
+ std::vector<LesserThanParams> no_equal_error_params = {
+ {-std::numeric_limits<double>::infinity(),
+ -std::numeric_limits<double>::infinity()},
+ {std::numeric_limits<double>::lowest(),
+ std::numeric_limits<double>::lowest()},
+ {-1, -1},
+ {std::numeric_limits<double>::min(), std::numeric_limits<double>::min()},
+ {0, 0},
+ {1, -1},
+ {1, 1},
+ {std::numeric_limits<double>::max(), std::numeric_limits<double>::max()},
+ {std::numeric_limits<double>::infinity(),
+ std::numeric_limits<double>::infinity()}};
+
+ for (LesserThanParams params : no_equal_error_params) {
+ EXPECT_THAT(
+ ValidateIsLesserThan(params.value, params.upper_bound, "Test value"),
+ StatusIs(absl::StatusCode::kInvalidArgument,
+ HasSubstr("Test value must be lesser than")));
+ }
+}
+
+TEST(ValidateTest, IsLesserThanOrEqualToOkStatus) {
+ struct LesserThanParams {
+ double value;
+ double upper_bound;
+ };
+
+ std::vector<LesserThanParams> success_params = {
+ {-std::numeric_limits<double>::infinity(),
+ -std::numeric_limits<double>::infinity()},
+ {std::numeric_limits<double>::lowest(),
+ std::numeric_limits<double>::lowest()},
+ {-1, -1},
+ {-1, 1},
+ {0, 0},
+ {std::numeric_limits<double>::min(), std::numeric_limits<double>::min()},
+ {
+ 1,
+ 1,
+ },
+ {std::numeric_limits<double>::max(), std::numeric_limits<double>::max()},
+ {std::numeric_limits<double>::infinity(),
+ std::numeric_limits<double>::infinity()}};
+
+ for (LesserThanParams params : success_params) {
+ EXPECT_OK(ValidateIsLesserThanOrEqualTo(params.value, params.upper_bound,
+ "Test value"));
+ }
+}
+
+TEST(ValidateTest, IsLesserThanOrEqualToError) {
+ struct LesserThanParams {
+ double value;
+ double upper_bound;
+ };
+
+ std::vector<LesserThanParams> or_equal_error_params = {
+ {std::numeric_limits<double>::lowest(),
+ -std::numeric_limits<double>::infinity()},
+ {std::numeric_limits<double>::min(), 0},
+ {1, -1},
+ {std::numeric_limits<double>::infinity(),
+ std::numeric_limits<double>::max()}};
+
+ for (LesserThanParams params : or_equal_error_params) {
+ EXPECT_THAT(
+ ValidateIsLesserThanOrEqualTo(params.value, params.upper_bound,
+ "Test value"),
+ StatusIs(absl::StatusCode::kInvalidArgument,
+ HasSubstr("Test value must be lesser than or equal to")));
+ }
+}
+
+TEST(ValidateTest, IsGreaterThanOkStatus) {
+ struct GreaterThanParams {
+ double value;
+ double lower_bound;
+ };
+
+ std::vector<GreaterThanParams> success_params = {
+ {std::numeric_limits<double>::lowest(),
+ -std::numeric_limits<double>::infinity()},
+ {std::numeric_limits<double>::min(), 0},
+ {1, -1},
+ {std::numeric_limits<double>::infinity(),
+ std::numeric_limits<double>::max()},
+ };
+
+ for (GreaterThanParams params : success_params) {
+ EXPECT_OK(
+ ValidateIsGreaterThan(params.value, params.lower_bound, "Test value"));
+ }
+}
+
+TEST(ValidateTest, IsGreaterThanError) {
+ struct GreaterThanParams {
+ double value;
+ double lower_bound;
+ };
+
+ std::vector<GreaterThanParams> no_equal_error_params = {
+ {-std::numeric_limits<double>::infinity(),
+ -std::numeric_limits<double>::infinity()},
+ {std::numeric_limits<double>::lowest(),
+ std::numeric_limits<double>::lowest()},
+ {-1, -1},
+ {std::numeric_limits<double>::min(), std::numeric_limits<double>::min()},
+ {0, 0},
+ {-1, 1},
+ {1, 1},
+ {std::numeric_limits<double>::max(), std::numeric_limits<double>::max()},
+ {std::numeric_limits<double>::infinity(),
+ std::numeric_limits<double>::infinity()}};
+
+ for (GreaterThanParams params : no_equal_error_params) {
+ EXPECT_THAT(
+ ValidateIsGreaterThan(params.value, params.lower_bound, "Test value"),
+ StatusIs(absl::StatusCode::kInvalidArgument,
+ HasSubstr("Test value must be greater than")));
+ }
+}
+
+TEST(ValidateTest, IsGreaterThanOrEqualToOkStatus) {
+ struct GreaterThanParams {
+ double value;
+ double lower_bound;
+ };
+
+ std::vector<GreaterThanParams> success_params = {
+ {-std::numeric_limits<double>::infinity(),
+ -std::numeric_limits<double>::infinity()},
+ {std::numeric_limits<double>::lowest(),
+ std::numeric_limits<double>::lowest()},
+ {-1, -1},
+ {0, 0},
+ {1, -1},
+ {std::numeric_limits<double>::min(), std::numeric_limits<double>::min()},
+ {1, 1},
+ {std::numeric_limits<double>::max(), std::numeric_limits<double>::max()},
+ {std::numeric_limits<double>::infinity(),
+ std::numeric_limits<double>::infinity()}};
+
+ for (GreaterThanParams params : success_params) {
+ EXPECT_OK(ValidateIsGreaterThanOrEqualTo(params.value, params.lower_bound,
+ "Test value"));
+ }
+}
+
+TEST(ValidateTest, IsGreaterThanOrEqualToError) {
+ struct GreaterThanParams {
+ double value;
+ double lower_bound;
+ };
+
+ std::vector<GreaterThanParams> or_equal_error_params = {
+ {-std::numeric_limits<double>::infinity(),
+ std::numeric_limits<double>::lowest()},
+ {0, std::numeric_limits<double>::min()},
+ {-1, 1},
+ {std::numeric_limits<double>::max(),
+ std::numeric_limits<double>::infinity()}};
+
+ for (GreaterThanParams params : or_equal_error_params) {
+ EXPECT_THAT(
+ ValidateIsGreaterThanOrEqualTo(params.value, params.lower_bound,
+ "Test value"),
+ StatusIs(absl::StatusCode::kInvalidArgument,
+ HasSubstr("Test value must be greater than or equal to")));
+ }
+}
+
+TEST(ValidateTest, IsInIntervalOkStatus) {
+ struct IntervalParams {
+ double value;
+ double lower_bound;
+ double upper_bound;
+ bool include_lower;
+ bool include_upper;
+ };
+
+ std::vector<IntervalParams> success_params = {
+ {std::numeric_limits<double>::lowest(),
+ std::numeric_limits<double>::lowest(),
+ std::numeric_limits<double>::lowest(), false, true},
+ {std::numeric_limits<double>::lowest(),
+ std::numeric_limits<double>::lowest(),
+ std::numeric_limits<double>::lowest(), true, false},
+ {std::numeric_limits<double>::lowest(),
+ std::numeric_limits<double>::lowest(),
+ std::numeric_limits<double>::lowest(), true, true},
+ {0, -1, 1, false, false},
+ {0, -1, 1, true, false},
+ {0, -1, 1, false, true},
+ {0, -1, 1, true, true},
+ {0, 0, 0, false, true},
+ {0, 0, 0, true, false},
+ {0, 0, 0, true, true},
+ {0.0, 0.0 - std::numeric_limits<double>::min(),
+ 0.0 + std::numeric_limits<double>::min(), false, false},
+ {-1, -1, 1, true, false},
+ {1, -1, 1, false, true},
+ {1, 1, 1, false, true},
+ {1, 1, 1, true, false},
+ {1, 1, 1, true, true},
+ {std::numeric_limits<double>::min(), std::numeric_limits<double>::min(),
+ std::numeric_limits<double>::min(), false, true},
+ {std::numeric_limits<double>::min(), std::numeric_limits<double>::min(),
+ std::numeric_limits<double>::min(), true, false},
+ {std::numeric_limits<double>::min(), std::numeric_limits<double>::min(),
+ std::numeric_limits<double>::min(), true, true},
+ {std::numeric_limits<double>::max(), std::numeric_limits<double>::max(),
+ std::numeric_limits<double>::max(), false, true},
+ {std::numeric_limits<double>::max(), std::numeric_limits<double>::max(),
+ std::numeric_limits<double>::max(), true, false},
+ {std::numeric_limits<double>::max(), std::numeric_limits<double>::max(),
+ std::numeric_limits<double>::max(), true, true},
+ };
+
+ for (IntervalParams params : success_params) {
+ EXPECT_OK(ValidateIsInInterval(params.value, params.lower_bound,
+ params.upper_bound, params.include_lower,
+ params.include_upper, "Test value"));
+ }
+}
+
+TEST(ValidateTest, IsOutsideExclusiveInterval) {
+ struct IntervalParams {
+ double value;
+ double lower_bound;
+ double upper_bound;
+ bool include_lower;
+ bool include_upper;
+ };
+
+ std::vector<IntervalParams> exclusive_error_params = {
+ {std::numeric_limits<double>::lowest(),
+ std::numeric_limits<double>::lowest(),
+ std::numeric_limits<double>::lowest(), false, false},
+ {-1, 0, 1, false, false},
+ {-1, -1, -1, false, false},
+ {0, 0, 0, false, false},
+ {1, 1, 1, false, false},
+ {std::numeric_limits<double>::min(), std::numeric_limits<double>::min(),
+ std::numeric_limits<double>::min(), false, false},
+ {std::numeric_limits<double>::max(), std::numeric_limits<double>::max(),
+ std::numeric_limits<double>::max(), false, false},
+ };
+
+ for (IntervalParams params : exclusive_error_params) {
+ EXPECT_THAT(
+ ValidateIsInInterval(params.value, params.lower_bound,
+ params.upper_bound, params.include_lower,
+ params.include_upper, "Test value"),
+ StatusIs(absl::StatusCode::kInvalidArgument,
+ HasSubstr("Test value must be in the exclusive interval (")));
+ }
+}
+
+TEST(ValidateTest, IsOutsideInclusiveInterval) {
+ struct IntervalParams {
+ double value;
+ double lower_bound;
+ double upper_bound;
+ bool include_lower;
+ bool include_upper;
+ };
+
+ std::vector<IntervalParams> inclusive_error_params = {
+ {-1, 0, 1, true, true},
+ {0 - std::numeric_limits<double>::min(), 0,
+ std::numeric_limits<double>::min(), true, true},
+ };
+
+ for (IntervalParams params : inclusive_error_params) {
+ EXPECT_THAT(
+ ValidateIsInInterval(params.value, params.lower_bound,
+ params.upper_bound, params.include_lower,
+ params.include_upper, "Test value"),
+ StatusIs(absl::StatusCode::kInvalidArgument,
+ HasSubstr("Test value must be in the inclusive interval [")));
+ }
+}
+
+TEST(ValidateTest, IsOutsideHalfClosedInterval) {
+ struct IntervalParams {
+ double value;
+ double lower_bound;
+ double upper_bound;
+ bool include_lower;
+ bool include_upper;
+ };
+
+ EXPECT_THAT(ValidateIsInInterval(-1, 0, 1, true, false, "Test value"),
+ StatusIs(absl::StatusCode::kInvalidArgument,
+ HasSubstr("Test value must be in the interval [0,1)")));
+
+ EXPECT_THAT(ValidateIsInInterval(-1, 0, 1, false, true, "Test value"),
+ StatusIs(absl::StatusCode::kInvalidArgument,
+ HasSubstr("Test value must be in the interval (0,1]")));
+
+ EXPECT_THAT(ValidateIsInInterval(-1, -1, 1, false, true, "Test value"),
+ StatusIs(absl::StatusCode::kInvalidArgument,
+ HasSubstr("Test value must be in the interval (-1,1]")));
+
+ EXPECT_THAT(ValidateIsInInterval(1, -1, 1, true, false, "Test value"),
+ StatusIs(absl::StatusCode::kInvalidArgument,
+ HasSubstr("Test value must be in the interval [-1,1)")));
+}
+
+// These tests document cases that result in known, incorrect behaviour
+TEST(ValidateTest, IsInIntervalBadBehaviour) {
+ struct IntervalParams {
+ double value;
+ double lower_bound;
+ double upper_bound;
+ bool include_lower;
+ bool include_upper;
+ };
+
+ std::vector<IntervalParams> bad_exclusive_error_params = {
+ // These test parameters should result in an OK_STATUS since the value is
+ // within the bounds, but instead returns a kInvalidArgument status
+ // because of double (im)precision.
+ {-1.0, -1.0 - std::numeric_limits<double>::min(),
+ -1.0 + std::numeric_limits<double>::min(), false, false},
+ {1.0, 1.0 - std::numeric_limits<double>::min(),
+ 1.0 + std::numeric_limits<double>::min(), false, false},
+ };
+
+ for (IntervalParams params : bad_exclusive_error_params) {
+ EXPECT_THAT(
+ ValidateIsInInterval(params.value, params.lower_bound,
+ params.upper_bound, params.include_lower,
+ params.include_upper, "Test value"),
+ StatusIs(absl::StatusCode::kInvalidArgument,
+ HasSubstr("Test value must be in the exclusive interval (")));
+ }
+
+ std::vector<IntervalParams> bad_success_params = {
+ // These test parameters should result in an kInvalidArgument status since
+ // the value falls outside of the bounds, but instead returns an OK_STATUS
+ // because of double (im)precision.
+ {-1.0 - std::numeric_limits<double>::min(), -1.0,
+ -1.0 + std::numeric_limits<double>::min(), true, true},
+ {1.0 - std::numeric_limits<double>::min(), 1.0,
+ 1.0 + std::numeric_limits<double>::min(), true, true},
+ };
+
+ for (IntervalParams params : bad_success_params) {
+ EXPECT_OK(ValidateIsInInterval(params.value, params.lower_bound,
+ params.upper_bound, params.include_lower,
+ params.include_upper, "Test value"));
+ }
+}
+
} // namespace
} // namespace differential_privacy
diff --git a/cc/docs/index.md b/cc/docs/index.md
index 19aefa2..7f6cf29 100644
--- a/cc/docs/index.md
+++ b/cc/docs/index.md
@@ -37,6 +37,9 @@
contributions should be reduced to a single input element before being passed to
our algorithms.
+Our library does not protect against integer overflows, since such protection
+could violate DP.
+
## Error codes
Our libraries use
diff --git a/cc/postgres/dp_func_test.cc b/cc/postgres/dp_func_test.cc
index bb9ce09..b5cfc75 100644
--- a/cc/postgres/dp_func_test.cc
+++ b/cc/postgres/dp_func_test.cc
@@ -31,7 +31,7 @@
TEST(DpCount, BadEpsilon) {
std::string err;
auto dp_count = DpCount(&err, false, 0);
- EXPECT_EQ(err, "Epsilon has to be positive but is 0");
+ EXPECT_EQ(err, "Epsilon must be finite and positive, but is 0.");
}
TEST(DpCount, AddEntryMissingAlgorithm) {
@@ -68,7 +68,8 @@
TEST(DpNtile, BadPercentile) {
std::string err;
auto func = DpNtile(&err, -1, 0, 10);
- EXPECT_EQ(err, "Percentile must be between 0 and 1.");
+ EXPECT_EQ(err,
+ "Percentile must be in the inclusive interval [0,1], but is -1.");
}
TEST(DpNtile, BadBounds) {
diff --git a/cc/postgres/install_extension.sh b/cc/postgres/install_extension.sh
index 412bad1..4eff702 100755
--- a/cc/postgres/install_extension.sh
+++ b/cc/postgres/install_extension.sh
@@ -16,14 +16,12 @@
#!/bin/bash
-echo "Currently set postgres directory:" $PG_DIR
-
set -ex
WORKSPACE_DIR=`bazel info workspace`
BIN_DIR=`bazel info -c opt bazel-bin`
-LIB_DIR=`ppg_config --pkglibdir`
+LIB_DIR=`pg_config --pkglibdir`
SHARE_DIR=`pg_config --sharedir`
bazel build -c opt //postgres:anon_func.so
diff --git a/cc/testing/stochastic_tester_test.cc b/cc/testing/stochastic_tester_test.cc
index 215b1a7..83f9e51 100644
--- a/cc/testing/stochastic_tester_test.cc
+++ b/cc/testing/stochastic_tester_test.cc
@@ -41,7 +41,7 @@
nullptr>
class NonDpSum : public Algorithm<T> {
public:
- NonDpSum() : Algorithm<T>(0), result_(0) {}
+ NonDpSum() : Algorithm<T>(1), result_(0) {}
void AddEntry(const T& t) override { result_ += t; }
base::StatusOr<Output> GenerateResult(
@@ -64,7 +64,7 @@
template <typename T>
class NonDpCount : public Algorithm<T> {
public:
- NonDpCount() : Algorithm<T>(0), result_(0) {}
+ NonDpCount() : Algorithm<T>(1), result_(0) {}
void AddEntry(const T& t) override { ++result_; }
base::StatusOr<Output> GenerateResult(
@@ -152,7 +152,7 @@
nullptr>
class AlwaysError : public Algorithm<T> {
public:
- AlwaysError() : Algorithm<T>(0), result_(0) {}
+ AlwaysError() : Algorithm<T>(1), result_(0) {}
void AddEntry(const T& t) override {}
base::StatusOr<Output> GenerateResult(
diff --git a/common_docs/Privacy_Loss_Distributions.pdf b/common_docs/Privacy_Loss_Distributions.pdf
index df326ac..13ff151 100644
--- a/common_docs/Privacy_Loss_Distributions.pdf
+++ b/common_docs/Privacy_Loss_Distributions.pdf
Binary files differ
diff --git a/go/noise/gaussian_noise.go b/go/noise/gaussian_noise.go
index 9e6d839..660aaff 100644
--- a/go/noise/gaussian_noise.go
+++ b/go/noise/gaussian_noise.go
@@ -67,7 +67,7 @@
}
sigma := SigmaForGaussian(l0Sensitivity, lInfSensitivity, epsilon, delta)
- return addGaussian(x, sigma)
+ return addGaussianFloat64(x, sigma)
}
// AddNoiseInt64 adds Gaussian noise to the specified int64, so that the
@@ -79,11 +79,7 @@
}
sigma := SigmaForGaussian(l0Sensitivity, float64(lInfSensitivity), epsilon, delta)
- // Calling addGaussian on 0.0 avoids casting x to a float64 value, which is not secure from a
- // privacy perspective as it can have unforeseen effects on the sensitivity of x. Rounding and
- // adding the resulting noise to x in a post processing step is a secure operation (for noise of
- // moderate magnitude, i.e. < 2^53).
- return int64(math.Round(addGaussian(0.0, sigma))) + x
+ return addGaussianInt64(x, sigma)
}
// Threshold returns the smallest threshold k to use in a differentially private
@@ -177,8 +173,8 @@
return checkArgsGaussian(label, l0Sensitivity, lInfSensitivity, epsilon, delta)
}
-// addGaussian adds Gaussian noise of scale σ to the specified float64.
-func addGaussian(x, sigma float64) float64 {
+// addGaussianFloat64 adds Gaussian noise of scale σ to the specified float64.
+func addGaussianFloat64(x, sigma float64) float64 {
granularity := ceilPowerOfTwo(2.0 * sigma / binomialBound)
// sqrtN is chosen in a way that places it in the interval between binomialBound
@@ -189,6 +185,21 @@
return roundToMultipleOfPowerOfTwo(x, granularity) + float64(sample)*granularity
}
+// addGaussianInt64 adds Gaussian noise of scale σ to the specified int64.
+func addGaussianInt64(x int64, sigma float64) int64 {
+ granularity := ceilPowerOfTwo(2.0 * sigma / binomialBound)
+
+ // sqrtN is chosen in a way that places it in the interval between binomialBound
+ // and binomialBound / 2. This ensures that the respective binomial distribution
+ // consists of enough Bernoulli samples to closely approximate a Gaussian distribution.
+ sqrtN := 2.0 * sigma / granularity
+ sample := symmetricBinomial(sqrtN)
+ if granularity < 1 {
+ return x + int64(math.Round(float64(sample)*granularity))
+ }
+ return roundToMultiple(x, int64(granularity)) + sample*int64(granularity)
+}
+
// computeConfidenceIntervalGaussian computes a confidence interval that contains the raw value x from which
// float64 noisedX is computed with a probability equal to 1 - alpha with the given sigma.
func computeConfidenceIntervalGaussian(noisedX, sigma, alpha float64) ConfidenceInterval {
diff --git a/go/noise/gaussian_noise_test.go b/go/noise/gaussian_noise_test.go
index 231cdd3..dee2148 100644
--- a/go/noise/gaussian_noise_test.go
+++ b/go/noise/gaussian_noise_test.go
@@ -18,6 +18,7 @@
import (
"math"
+ "math/rand"
"testing"
"github.com/grd/stat"
@@ -271,6 +272,109 @@
}
}
+func TestAddGaussianFloat64RoundsToGranularity(t *testing.T) {
+ const numberOfTrials = 1000
+ for _, tc := range []struct {
+ sigma float64
+ wantGranularity float64
+ }{
+ {
+ sigma: 6.8e10,
+ wantGranularity: 1.0 / 1048576.0,
+ },
+ {
+ sigma: 7.0e13,
+ wantGranularity: 1.0 / 1024.0,
+ },
+ {
+ sigma: 2.2e15,
+ wantGranularity: 1.0 / 32,
+ },
+ {
+ sigma: 1.7e16,
+ wantGranularity: 1.0 / 4.0,
+ },
+ {
+ sigma: 3.5e16,
+ wantGranularity: 1.0 / 2.0,
+ },
+ {
+ sigma: 7.0e16,
+ wantGranularity: 1.0,
+ },
+ {
+ sigma: 1.0e17,
+ wantGranularity: 2.0,
+ },
+ {
+ sigma: 2.0e17,
+ wantGranularity: 4.0,
+ },
+ {
+ sigma: 2.3e18,
+ wantGranularity: 32.0,
+ },
+ {
+ sigma: 7.0e19,
+ wantGranularity: 1024.0,
+ },
+ {
+ sigma: 7.5e22,
+ wantGranularity: 1048576.0,
+ },
+ } {
+ for i := 0; i < numberOfTrials; i++ {
+ // the input x of addGaussianFloat64 can be arbitrary
+ x := rand.Float64()*tc.wantGranularity*10 - tc.wantGranularity*5
+ noisedX := addGaussianFloat64(x, tc.sigma)
+ if math.Round(noisedX/tc.wantGranularity) != noisedX/tc.wantGranularity {
+ t.Errorf("Got noised x: %f, not a multiple of: %f", noisedX, tc.wantGranularity)
+ break
+ }
+ }
+ }
+}
+
+func TestAddGaussianInt64RoundsToGranularity(t *testing.T) {
+ const numberOfTrials = 1000
+ for _, tc := range []struct {
+ sigma float64
+ wantGranularity int64
+ }{
+ {
+ sigma: 1.0e17,
+ wantGranularity: 2,
+ },
+ {
+ sigma: 2.0e17,
+ wantGranularity: 4,
+ },
+ {
+ sigma: 2.3e18,
+ wantGranularity: 32,
+ },
+ {
+ sigma: 7.0e19,
+ wantGranularity: 1024,
+ },
+ {
+ sigma: 7.5e22,
+ wantGranularity: 1048576,
+ },
+ } {
+ for i := 0; i < numberOfTrials; i++ {
+ // the input x of addGaussianInt64 can be arbitrary but should cover all congruence
+ // classes of the anticipated granularity
+ x := rand.Int63n(tc.wantGranularity*10) - tc.wantGranularity*5
+ noisedX := addGaussianInt64(x, tc.sigma)
+ if noisedX%tc.wantGranularity != 0 {
+ t.Errorf("Got noised x: %d, not devisible by: %d", noisedX, tc.wantGranularity)
+ break
+ }
+ }
+ }
+}
+
func TestSigmaForGaussianInvertsDeltaForGaussian(t *testing.T) {
// For these tests, we specify the value of sigma that we want to compute and
// use DeltaForGaussian to determine the corresponding delta. We then verify
diff --git a/go/noise/laplace_noise.go b/go/noise/laplace_noise.go
index 3036939..d02504b 100644
--- a/go/noise/laplace_noise.go
+++ b/go/noise/laplace_noise.go
@@ -64,7 +64,7 @@
log.Fatalf("laplace.AddNoiseFloat64(l0sensitivity %d, lInfSensitivity %f, epsilon %f, delta %e) checks failed with %v",
l0Sensitivity, lInfSensitivity, epsilon, delta, err)
}
- return addLaplace(x, epsilon, lInfSensitivity*float64(l0Sensitivity) /* l1Sensitivity */)
+ return addLaplaceFloat64(x, epsilon, lInfSensitivity*float64(l0Sensitivity) /* l1Sensitivity */)
}
// AddNoiseInt64 adds Laplace noise to the specified int64 x so that the
@@ -75,11 +75,7 @@
log.Fatalf("laplace.AddNoiseInt64(l0sensitivity %d, lInfSensitivity %d, epsilon %f, delta %e) checks failed with %v",
l0Sensitivity, lInfSensitivity, epsilon, delta, err)
}
- // Calling addLaplace on 0.0 avoids casting x to a float64 value, which is not secure from a
- // privacy perspective as it can have unforeseen effects on the sensitivity of x. Rounding and
- // adding the resulting noise to x in a post processing step is a secure operation (for noise of
- // moderate magnitude, i.e. < 2^53).
- return int64(math.Round(addLaplace(0.0, epsilon, float64(lInfSensitivity*l0Sensitivity) /* l1Sensitivity */))) + x
+ return addLaplaceInt64(x, epsilon, lInfSensitivity*l0Sensitivity /* l1Sensitivity */)
}
// Threshold returns the smallest threshold k to use in a differentially private
@@ -220,14 +216,25 @@
return checkArgsLaplace(label, l0Sensitivity, lInfSensitivity, epsilon, delta)
}
-// addLaplace adds Laplace noise scaled to the given epsilon and l1Sensitivity to the
+// addLaplaceFloat64 adds Laplace noise scaled to the given epsilon and l1Sensitivity to the
// specified float64
-func addLaplace(x, epsilon, l1Sensitivity float64) float64 {
+func addLaplaceFloat64(x, epsilon, l1Sensitivity float64) float64 {
granularity := ceilPowerOfTwo((l1Sensitivity / epsilon) / granularityParam)
sample := twoSidedGeometric(granularity * epsilon / (l1Sensitivity + granularity))
return roundToMultipleOfPowerOfTwo(x, granularity) + float64(sample)*granularity
}
+// addLaplaceInt64 adds Laplace noise scaled to the given epsilon and l1Sensitivity to the
+// specified int64
+func addLaplaceInt64(x int64, epsilon float64, l1Sensitivity int64) int64 {
+ granularity := ceilPowerOfTwo((float64(l1Sensitivity) / epsilon) / granularityParam)
+ sample := twoSidedGeometric(granularity * epsilon / (float64(l1Sensitivity) + granularity))
+ if granularity < 1 {
+ return x + int64(math.Round(float64(sample)*granularity))
+ }
+ return roundToMultiple(x, int64(granularity)) + sample*int64(granularity)
+}
+
// laplaceLambda computes the scale parameter λ for the Laplace noise
// distribution required by the Laplace mechanism for achieving ε-differential
// privacy on databases with the given L_0 and L_∞ sensitivities.
diff --git a/go/noise/laplace_noise_test.go b/go/noise/laplace_noise_test.go
index ab858a3..c25c17a 100644
--- a/go/noise/laplace_noise_test.go
+++ b/go/noise/laplace_noise_test.go
@@ -18,6 +18,7 @@
import (
"math"
+ "math/rand"
"testing"
"github.com/grd/stat"
@@ -101,6 +102,127 @@
}
}
+func TestAddLaplaceFloat64RoundsToGranularity(t *testing.T) {
+ const numberOfTrials = 1000
+ for _, tc := range []struct {
+ epsilon float64
+ l1Sensitivity float64
+ wantGranularity float64
+ }{
+ {
+ epsilon: 9.6e-7,
+ l1Sensitivity: 1.0,
+ wantGranularity: 1.0 / 1048576.0,
+ },
+ {
+ epsilon: 4.7e-10,
+ l1Sensitivity: 1.0,
+ wantGranularity: 1.0 / 1024.0,
+ },
+ {
+ epsilon: 1.5e-11,
+ l1Sensitivity: 1.0,
+ wantGranularity: 1.0 / 32.0,
+ },
+ {
+ epsilon: 3.7e-12,
+ l1Sensitivity: 1.0,
+ wantGranularity: 1.0 / 4.0,
+ },
+ {
+ epsilon: 1.9e-12,
+ l1Sensitivity: 1.0,
+ wantGranularity: 1.0 / 2.0,
+ },
+ {
+ epsilon: 9.1e-13,
+ l1Sensitivity: 1.0,
+ wantGranularity: 1.0,
+ },
+ {
+ epsilon: 4.6e-13,
+ l1Sensitivity: 1.0,
+ wantGranularity: 2.0,
+ },
+ {
+ epsilon: 2.8e-13,
+ l1Sensitivity: 1.0,
+ wantGranularity: 4.0,
+ },
+ {
+ epsilon: 2.9e-14,
+ l1Sensitivity: 1.0,
+ wantGranularity: 32.0,
+ },
+ {
+ epsilon: 8.9e-16,
+ l1Sensitivity: 1.0,
+ wantGranularity: 1024.0,
+ },
+ {
+ epsilon: 8.7e-19,
+ l1Sensitivity: 1.0,
+ wantGranularity: 1048576.0,
+ },
+ } {
+ for i := 0; i < numberOfTrials; i++ {
+ // the input x of addLaplaceFloat64 can be arbitrary
+ x := rand.Float64()*tc.wantGranularity*10 - tc.wantGranularity*5
+ noisedX := addLaplaceFloat64(x, tc.epsilon, tc.l1Sensitivity)
+ if math.Round(noisedX/tc.wantGranularity) != noisedX/tc.wantGranularity {
+ t.Errorf("Got noised x: %f, not a multiple of: %f", noisedX, tc.wantGranularity)
+ break
+ }
+ }
+ }
+}
+
+func TestAddLaplaceInt64RoundsToGranularity(t *testing.T) {
+ const numberOfTrials = 1000
+ for _, tc := range []struct {
+ epsilon float64
+ l1Sensitivity int64
+ wantGranularity int64
+ }{
+ {
+ epsilon: 4.6e-13,
+ l1Sensitivity: 1,
+ wantGranularity: 2,
+ },
+ {
+ epsilon: 2.8e-13,
+ l1Sensitivity: 1,
+ wantGranularity: 4,
+ },
+ {
+ epsilon: 2.9e-14,
+ l1Sensitivity: 1,
+ wantGranularity: 32,
+ },
+ {
+ epsilon: 8.9e-16,
+ l1Sensitivity: 1,
+ wantGranularity: 1024,
+ },
+ {
+ epsilon: 8.7e-19,
+ l1Sensitivity: 1,
+ wantGranularity: 1048576,
+ },
+ } {
+ for i := 0; i < numberOfTrials; i++ {
+ // the input x of addLaplaceInt64 can be arbitrary but should cover all congruence
+ // classes of the anticipated granularity
+ x := rand.Int63n(tc.wantGranularity*10) - tc.wantGranularity*5
+ noisedX := addLaplaceInt64(x, tc.epsilon, tc.l1Sensitivity)
+ if noisedX%tc.wantGranularity != 0 {
+ t.Errorf("Got noised x: %d, not devisible by: %d", noisedX, tc.wantGranularity)
+ break
+ }
+ }
+ }
+}
+
func TestThresholdLaplace(t *testing.T) {
// For the l0Sensitivity=1 cases, we make certain that we have implemented
// both tails of the Laplace distribution. To do so, we write tests in pairs by
diff --git a/go/noise/secure_noise_math.go b/go/noise/secure_noise_math.go
index 6c4fa74..d318c5a 100644
--- a/go/noise/secure_noise_math.go
+++ b/go/noise/secure_noise_math.go
@@ -65,6 +65,26 @@
return math.Round(x/granularity) * granularity
}
+// roundToMultiple returns a multiple of granularity that is closest to x.
+// The result is exact.
+func roundToMultiple(x, granularity int64) int64 {
+ result := (x / granularity) * granularity
+ if x >= 0 {
+ if x-result >= (result+granularity)-x {
+ // round up
+ return result + granularity
+ }
+ // round down
+ return result
+ }
+ if x-(result-granularity) >= result-x {
+ // round up
+ return result
+ }
+ // round down
+ return result - granularity
+}
+
// nextLargerFloat64 computes the smallest float64 value that is larger than or equal to the provided int64 value.
//
// Mapping from int64 to float64 for large int64 values (> 2^53) is inaccurate since they cannot be
diff --git a/go/noise/secure_noise_math_test.go b/go/noise/secure_noise_math_test.go
index 7ddd3a1..7f92f56 100644
--- a/go/noise/secure_noise_math_test.go
+++ b/go/noise/secure_noise_math_test.go
@@ -79,6 +79,211 @@
}
}
+func TestRoundToMultipleGranularityIsOne(t *testing.T) {
+ // Verify that RoundToMultiple returns x if granularity is 1
+ for _, x := range []int64{0, 1, -1, 2, -2, 648391, -648391} {
+ got := roundToMultiple(x, 1)
+ if got != x {
+ t.Errorf(
+ "roundToMultipleOfPowerOfTwo(%d, 1) = %d, want %d",
+ x,
+ got,
+ x,
+ )
+ }
+ }
+}
+
+func TestRoundToMultipleXIsEven(t *testing.T) {
+ for _, tc := range []struct {
+ x int64
+ granularity int64
+ want int64
+ }{
+ {
+ x: 0,
+ granularity: 4,
+ want: 0,
+ },
+ {
+ x: 1,
+ granularity: 4,
+ want: 0,
+ },
+ {
+ x: 2,
+ granularity: 4,
+ want: 4,
+ },
+ {
+ x: 3,
+ granularity: 4,
+ want: 4,
+ },
+ {
+ x: 4,
+ granularity: 4,
+ want: 4,
+ },
+ {
+ x: -1,
+ granularity: 4,
+ want: 0,
+ },
+ {
+ x: -2,
+ granularity: 4,
+ want: 0,
+ },
+ {
+ x: -3,
+ granularity: 4,
+ want: -4,
+ },
+ {
+ x: -4,
+ granularity: 4,
+ want: -4,
+ },
+ {
+ x: 648389,
+ granularity: 4,
+ want: 648388,
+ },
+ {
+ x: 648390,
+ granularity: 4,
+ want: 648392,
+ },
+ {
+ x: 648391,
+ granularity: 4,
+ want: 648392,
+ },
+ {
+ x: 648392,
+ granularity: 4,
+ want: 648392,
+ },
+ {
+ x: -648389,
+ granularity: 4,
+ want: -648388,
+ },
+ {
+ x: -648390,
+ granularity: 4,
+ want: -648388,
+ },
+ {
+ x: -648391,
+ granularity: 4,
+ want: -648392,
+ },
+ {
+ x: -648392,
+ granularity: 4,
+ want: -648392,
+ },
+ } {
+ got := roundToMultiple(tc.x, tc.granularity)
+ if got != tc.want {
+ t.Errorf(
+ "roundToMultiple(%d, %d) = %d, want %d",
+ tc.x,
+ tc.granularity,
+ got,
+ tc.want,
+ )
+ }
+ }
+}
+
+func TestRoundToMultipleXIsOdd(t *testing.T) {
+ for _, tc := range []struct {
+ x int64
+ granularity int64
+ want int64
+ }{
+ {
+ x: 0,
+ granularity: 3,
+ want: 0,
+ },
+ {
+ x: 1,
+ granularity: 3,
+ want: 0,
+ },
+ {
+ x: 2,
+ granularity: 3,
+ want: 3,
+ },
+ {
+ x: 3,
+ granularity: 3,
+ want: 3,
+ },
+ {
+ x: -1,
+ granularity: 3,
+ want: 0,
+ },
+ {
+ x: -2,
+ granularity: 3,
+ want: -3,
+ },
+ {
+ x: -3,
+ granularity: 3,
+ want: -3,
+ },
+ {
+ x: 648391,
+ granularity: 3,
+ want: 648390,
+ },
+ {
+ x: 648392,
+ granularity: 3,
+ want: 648393,
+ },
+ {
+ x: 648393,
+ granularity: 3,
+ want: 648393,
+ },
+ {
+ x: -648391,
+ granularity: 3,
+ want: -648390,
+ },
+ {
+ x: -648392,
+ granularity: 3,
+ want: -648393,
+ },
+ {
+ x: -648393,
+ granularity: 3,
+ want: -648393,
+ },
+ } {
+ got := roundToMultiple(tc.x, tc.granularity)
+ if got != tc.want {
+ t.Errorf(
+ "roundToMultiple(%d, %d) = %d, want %d",
+ tc.x,
+ tc.granularity,
+ got,
+ tc.want,
+ )
+ }
+ }
+}
+
func TestRoundToMultipleOfPowerOfTwoXIsAMultiple(t *testing.T) {
// Verify that RoundToMultipleOfPowerOfTwo returns x if x is a
// multiple of granularity.