Fix a privacy-impacting bug in ThresholdedResult() function of dpagg.Count (#101)

Fix a privacy-impacting bug in ThresholdedResult() function of dpagg.Count that leads to slightly higher than intended delta. The bug is a result of converting floating-point threshold to an integer. Consider the following to see how the bug would occur:

Assume the noisy count is 37 and the threshold computed from noise parameters & threshold delta is 37.3. This means that we should not be returning the result. However, converting the threshold (37.3) to an int64 truncates the decimal part per go specification, making it 37. Threshold (37) is smaller than or equal to noisy result (37), so we return the result.

To fix this, we round the threshold up to the nearest integer before converting it to an int64.

Although this problem did not exist for dpagg.BoundedSumInt64, we modify the code there for consistency.
diff --git a/go/dpagg/count.go b/go/dpagg/count.go
index 283de65..5e31dc0 100644
--- a/go/dpagg/count.go
+++ b/go/dpagg/count.go
@@ -173,13 +173,18 @@
 	return c.noisedCount
 }
 
-// ThresholdedResult is similar to Result() but applies thresholding to the
-// result. So, if the result is less than the threshold specified by the noise
-// mechanism, it returns nil. Otherwise, it returns the result.
+// ThresholdedResult is similar to Result() but applies thresholding to the result.
+// So, if the result is less than the threshold specified by the parameters of Count
+// as well as thresholdDelta, it returns nil. Otherwise, it returns the result.
+//
+// Note that the nil results should not be published when the existence of a
+// partition in the output depends on private data.
 func (c *Count) ThresholdedResult(thresholdDelta float64) *int64 {
 	threshold := c.Noise.Threshold(c.l0Sensitivity, float64(c.lInfSensitivity), c.epsilon, c.delta, thresholdDelta)
 	result := c.Result()
-	if result < int64(threshold) {
+	// Rounding up the threshold when converting it to int64 to ensure that no DP guarantees
+	// are violated due to a result being returned that is less than the fractional threshold.
+	if result < int64(math.Ceil(threshold)) {
 		return nil
 	}
 	return &result
diff --git a/go/dpagg/count_test.go b/go/dpagg/count_test.go
index ca081d6..0303663 100644
--- a/go/dpagg/count_test.go
+++ b/go/dpagg/count_test.go
@@ -339,14 +339,14 @@
 }
 
 func TestCountThresholdedResult(t *testing.T) {
-	// ThresholdedResult outputs the result when it is greater than the threshold (5 using noNoise)
+	// ThresholdedResult outputs the result when it is greater than the threshold (5.00001 using noNoise)
 	c1 := getNoiselessCount()
 	for i := 0; i < 10; i++ {
 		c1.Increment()
 	}
 	got := c1.ThresholdedResult(tenten)
 	if got == nil || *got != 10 {
-		t.Errorf("ThresholdedResult(%f): when 10 addings got %v, want 10", tenten, got)
+		t.Errorf("ThresholdedResult(%f): after 10 entries got %v, want 10", tenten, got)
 	}
 
 	// ThresholdedResult outputs nil when it is less than the threshold
@@ -355,7 +355,17 @@
 	c2.Increment()
 	got = c2.ThresholdedResult(tenten)
 	if got != nil {
-		t.Errorf("ThresholdedResult(%f): when 2 addings got %v, want nil", tenten, got)
+		t.Errorf("ThresholdedResult(%f): after 2 entries got %v, want nil", tenten, got)
+	}
+
+	// Edge case when noisy result is 5 and threshold is 5.00001, ThresholdedResult outputs nil.
+	c3 := getNoiselessCount()
+	for i := 0; i < 5; i++ {
+		c3.Increment()
+	}
+	got = c3.ThresholdedResult(tenten)
+	if got != nil {
+		t.Errorf("ThresholdedResult(%f): after 5 entries got %v, want nil", tenten, got)
 	}
 }
 
diff --git a/go/dpagg/dpagg_test.go b/go/dpagg/dpagg_test.go
index 9d76b11..f058e44 100644
--- a/go/dpagg/dpagg_test.go
+++ b/go/dpagg/dpagg_test.go
@@ -61,7 +61,7 @@
 }
 
 func (noNoise) Threshold(_ int64, _, _, _, _ float64) float64 {
-	return 5
+	return 5.00001
 }
 
 // If noNoise is not initialized with a noise distribution, confidence interval functions will return a default confidence interval, i.e [0,0].
diff --git a/go/dpagg/sum.go b/go/dpagg/sum.go
index bec62b2..e8aee51 100644
--- a/go/dpagg/sum.go
+++ b/go/dpagg/sum.go
@@ -254,15 +254,19 @@
 	return bs.noisedSum
 }
 
-// ThresholdedResult is similar to Result() but applies thresholding to the
-// result. So, if the result is less than the threshold specified by the noise
-// mechanism, it returns nil. Otherwise, it returns the result.
+// ThresholdedResult is similar to Result() but applies thresholding to the result.
+// So, if the result is less than the threshold specified by the parameters of
+// BoundedSumInt64 as well as thresholdDelta, it returns nil. Otherwise, it returns
+// the result.
+//
+// Note that the nil results should not be published when the existence of a
+// partition in the output depends on private data.
 func (bs *BoundedSumInt64) ThresholdedResult(thresholdDelta float64) *int64 {
 	threshold := bs.Noise.Threshold(bs.l0Sensitivity, float64(bs.lInfSensitivity), bs.epsilon, bs.delta, thresholdDelta)
 	result := bs.Result()
-	// To make sure floating-point rounding doesn't break DP guarantees, we err on
-	// the side of dropping the result if it is exactly equal to the threshold.
-	if float64(result) <= threshold {
+	// Rounding up the threshold when converting it to int64 to ensure that no DP guarantees
+	// are violated due to a result being returned that is less than the fractional threshold.
+	if result < int64(math.Ceil(threshold)) {
 		return nil
 	}
 	return &result
diff --git a/go/dpagg/sum_test.go b/go/dpagg/sum_test.go
index df76f65..8c742af 100644
--- a/go/dpagg/sum_test.go
+++ b/go/dpagg/sum_test.go
@@ -868,29 +868,38 @@
 }
 
 func TestThresholdedResultInt64(t *testing.T) {
-	// ThresholdedResult outputs the result when it is more than the threshold (5 using noNoise)
+	// ThresholdedResult outputs the result when it is more than the threshold (5.00001 using noNoise)
 	bs1 := getNoiselessBSI()
 	bs1.Add(1)
 	bs1.Add(2)
 	bs1.Add(3)
 	bs1.Add(4)
-	got := bs1.ThresholdedResult(5)
+	got := bs1.ThresholdedResult(0.1)
 	if got == nil || *got != 10 {
-		t.Errorf("ThresholdedResult(5): when 1, 2, 3, 4 were added got %v, want 10", got)
+		t.Errorf("ThresholdedResult(0.1): when 1, 2, 3, 4 were added got %v, want 10", got)
 	}
 
 	// ThresholdedResult outputs nil when it is less than the threshold
 	bs2 := getNoiselessBSI()
 	bs2.Add(1)
 	bs2.Add(2)
-	got = bs2.ThresholdedResult(5) // the parameter here is for the reader's eyes, the actual threshold value (5) is specified in noNoise.Threshold()
+	got = bs2.ThresholdedResult(0.1)
 	if got != nil {
-		t.Errorf("ThresholdedResult(5): when 1,2 were added got %v, want nil", got)
+		t.Errorf("ThresholdedResult(0.1): when 1,2 were added got %v, want nil", got)
+	}
+
+	// Edge case when noisy result is 5 and threshold is 5.00001, ThresholdedResult outputs nil.
+	bs3 := getNoiselessBSI()
+	bs3.Add(2)
+	bs3.Add(3)
+	got = bs3.ThresholdedResult(0.1)
+	if got != nil {
+		t.Errorf("ThresholdedResult(0.1): when 2,3 were added got %v, want nil", got)
 	}
 }
 
 func TestThresholdedResultFloat64(t *testing.T) {
-	// ThresholdedResult outputs the result when it is more than the threshold (5 using noNoise)
+	// ThresholdedResult outputs the result when it is more than the threshold (5.00001 using noNoise)
 	bs1 := getNoiselessBSF()
 	bs1.Add(1.5)
 	bs1.Add(2.5)
@@ -905,9 +914,9 @@
 	bs2 := getNoiselessBSF()
 	bs2.Add(1)
 	bs2.Add(2.5)
-	got = bs2.ThresholdedResult(5) // the parameter here is for the reader's eyes, the actual threshold value (5) is specified in noNoise.Threshold()
+	got = bs2.ThresholdedResult(0.1)
 	if got != nil {
-		t.Errorf("ThresholdedResult(5): when 1, 2.5 were added got %v, want nil", got)
+		t.Errorf("ThresholdedResult(0.1): when 1, 2.5 were added got %v, want nil", got)
 	}
 }