diff --git a/bitcoin/bloom.py b/bitcoin/bloom.py index d0e3b765..7dfa16a8 100644 --- a/bitcoin/bloom.py +++ b/bitcoin/bloom.py @@ -107,8 +107,9 @@ def __init__(self, nElements, nFPRate, nTweak, nFlags): """ LN2SQUARED = 0.4804530139182014246671025263266649717305529515945455 LN2 = 0.6931471805599453094172321214581765680755001343602552 - self.vData = bytearray(int(min(-1 / LN2SQUARED * nElements * math.log(nFPRate), self.MAX_BLOOM_FILTER_SIZE * 8) / 8)) - self.nHashFuncs = int(min(len(self.vData) * 8 / nElements * LN2, self.MAX_HASH_FUNCS)) + # Rounding must not produce a filter that matches every element. + self.vData = bytearray(max(1, int(min(-1 / LN2SQUARED * nElements * math.log(nFPRate), self.MAX_BLOOM_FILTER_SIZE * 8) / 8))) + self.nHashFuncs = max(1, int(min(len(self.vData) * 8 / nElements * LN2, self.MAX_HASH_FUNCS))) self.nTweak = nTweak self.nFlags = nFlags diff --git a/bitcoin/tests/test_bloom.py b/bitcoin/tests/test_bloom.py index 436a97ea..6cad37f6 100644 --- a/bitcoin/tests/test_bloom.py +++ b/bitcoin/tests/test_bloom.py @@ -40,6 +40,20 @@ def T(expected, seed, data): class Test_CBloomFilter(unittest.TestCase): + def test_small_and_high_false_positive_rate_filters(self): + for elements in (1, 2, 10, 100): + for rate in (0.05, 0.1, 0.5, 0.9, 0.9999): + filter = CBloomFilter(elements, rate, 0, CBloomFilter.UPDATE_ALL) + # An empty filter must not match everything, even when its + # calculated storage size or hash count rounds down to zero. + self.assertFalse(filter.contains(b'not inserted')) + filter.insert(b'inserted') + self.assertTrue(filter.contains(b'inserted')) + self.assertTrue(filter.IsWithinSizeConstraints()) + restored = CBloomFilter.deserialize(filter.serialize()) + self.assertTrue(restored.contains(b'inserted')) + self.assertEqual(restored.serialize(), filter.serialize()) + def test_create_insert_serialize(self): filter = CBloomFilter(3, 0.01, 0, CBloomFilter.UPDATE_ALL)