diff --git a/NBitcoin.Tests/Bip158_tests.cs b/NBitcoin.Tests/Bip158_tests.cs index b83f97925b..2a3f4d14d6 100644 --- a/NBitcoin.Tests/Bip158_tests.cs +++ b/NBitcoin.Tests/Bip158_tests.cs @@ -79,10 +79,13 @@ public void BuildFilterAndMatchValues() .Build(); var testKey = key.ToBytes().SafeSubarray(0, 16); + var reader = filter.GetNewGRStreamReader(); + // The filter should match all the values that were added. foreach (var name in names) { Assert.True(filter.Match(name, testKey)); + Assert.True(filter.Match(name, testKey, reader)); } // The filter should NOT match any extra value. @@ -445,9 +448,10 @@ public void RealScriptPubKeyFilterTest() var filter = builder.Build(); var keyMatch = key.ToBytes().SafeSubarray(0, 16); + var reader = filter.GetNewGRStreamReader(); foreach (var script in scripts) { - var match = filter.MatchAny(new[] { script.ToBytes() }, keyMatch); + var match = filter.Match(script.ToBytes(), keyMatch, reader); Assert.True(match); } } diff --git a/NBitcoin/BIP158/BitStream.cs b/NBitcoin/BIP158/BitStream.cs index d19d91ab49..b08d0031fa 100644 --- a/NBitcoin/BIP158/BitStream.cs +++ b/NBitcoin/BIP158/BitStream.cs @@ -1,5 +1,5 @@ using System; -using System.Collections; +using System.Collections.Generic; namespace NBitcoin { @@ -170,7 +170,6 @@ private void EnsureCapacity() } } - internal class GRCodedStreamWriter { private readonly BitStream _stream; @@ -204,14 +203,46 @@ public void Write(ulong value) } } - internal class GRCodedStreamReader + public class CachedGRCodedStreamReader : GRCodedStreamReader + { + private readonly List _cachedValues; + private int _position; + + internal CachedGRCodedStreamReader(BitStream stream, byte p, ulong lastValue) : base(stream, p, lastValue) + { + _cachedValues = new List(10_000); + _position = 0; + } + + public override bool TryRead(out ulong value) + { + if (_position >= _cachedValues.Count) + { + if (!base.TryRead(out var readValue)) + { + value = 0; + return false; + } + _cachedValues.Add(readValue); + } + value = _cachedValues[_position++]; + return true; + } + + internal override void ResetPosition() + { + _position = 0; + } + } + + public class GRCodedStreamReader { private readonly BitStream _stream; private readonly byte _p; private readonly ulong _modP; private ulong _lastValue; - public GRCodedStreamReader(BitStream stream, byte p, ulong lastValue) + internal GRCodedStreamReader(BitStream stream, byte p, ulong lastValue) { _stream = stream; _p = p; @@ -219,7 +250,7 @@ public GRCodedStreamReader(BitStream stream, byte p, ulong lastValue) _lastValue = lastValue; } - public bool TryRead(out ulong value) + public virtual bool TryRead(out ulong value) { if (TryReadUInt64(out var readedValue)) { @@ -233,6 +264,8 @@ public bool TryRead(out ulong value) return false; } + internal virtual void ResetPosition () {} + private bool TryReadUInt64(out ulong value) { value = 0U; diff --git a/NBitcoin/BIP158/GolombRiceFilter.cs b/NBitcoin/BIP158/GolombRiceFilter.cs index 5f17f2831b..37b28a6159 100644 --- a/NBitcoin/BIP158/GolombRiceFilter.cs +++ b/NBitcoin/BIP158/GolombRiceFilter.cs @@ -156,10 +156,23 @@ public uint256 GetHeader(uint256 previousHeader) /// Key used for hashing the data elements. /// true if the element is in the filter, otherwise false. public bool Match(byte[] data, byte[] key) + { + var reader = new GRCodedStreamReader(new BitStream(Data), P, 0); + return Match(data, key, reader); + } + + /// + /// Checks if the value passed is in the filter. + /// + /// Data element to check in the filter. + /// Key used for hashing the data elements. + /// Golomb-Rice stream reader. + /// true if the element is in the filter, otherwise false. + public bool Match(byte[] data, byte[] key, GRCodedStreamReader reader) { if (data == null) throw new ArgumentNullException(nameof(data)); - return MatchAny(new[] { data }, 1, key); + return MatchAny(new[] { data }, 1, key, reader); } /// @@ -170,7 +183,8 @@ public bool Match(byte[] data, byte[] key) /// true if at least one of the elements is in the filter, otherwise false. public bool MatchAny(byte[][] data, byte[] key) { - return MatchAny(data, data.Length, key); + var reader = new GRCodedStreamReader(new BitStream(Data), P, 0); + return MatchAny(data, data.Length, key, reader); } /// @@ -180,20 +194,33 @@ public bool MatchAny(byte[][] data, byte[] key) /// Key used for hashing the data elements. /// true if at least one of the elements is in the filter, otherwise false. public bool MatchAny(IEnumerable data, byte[] key) + { + var reader = new GRCodedStreamReader(new BitStream(Data), P, 0); + return MatchAny(data, key, reader); + } + + /// + /// Checks if any of the provided elements is in the filter. + /// + /// Data elements to check in the filter. + /// Key used for hashing the data elements. + /// Golomb-Rice stream reader. + /// true if at least one of the elements is in the filter, otherwise false. + public bool MatchAny(IEnumerable data, byte[] key, GRCodedStreamReader reader) { if (data == null) throw new ArgumentNullException(nameof(data)); if (data is byte[][] dataArray) { - return MatchAny(dataArray, dataArray.Length, key); + return MatchAny(dataArray, dataArray.Length, key, reader); } else if (data is ICollection dataCollection) { - return MatchAny(dataCollection, dataCollection.Count, key); + return MatchAny(dataCollection, dataCollection.Count, key, reader); } else { - return MatchAny(data, data.Count(), key); + return MatchAny(data, data.Count(), key, reader); } } @@ -203,7 +230,19 @@ public bool MatchAny(IEnumerable data, byte[] key) /// Data elements to check in the filter. /// Key used for hashing the data elements. /// true if at least one of the elements is in the filter, otherwise false. - internal bool MatchAny(IEnumerable data, int dataCount, byte[] key) + internal bool MatchAny(IEnumerable data, int dataCount, byte[] key, GRCodedStreamReader reader) + { + try + { + return InternalMatchAny(data, dataCount, key, reader); + } + finally + { + reader.ResetPosition(); + } + } + + private bool InternalMatchAny(IEnumerable data, int dataCount, byte[] key, GRCodedStreamReader sr) { if (data == null || dataCount == 0) throw new ArgumentException("data can not be null or empty array.", nameof(data)); @@ -212,9 +251,6 @@ internal bool MatchAny(IEnumerable data, int dataCount, byte[] key) var hs = ConstructHashedSet(P, N, M, key, data, dataCount); - var bitStream = new BitStream(Data); - var sr = new GRCodedStreamReader(bitStream, P, 0); - while(sr.TryRead(out var val)) { var dataIndex = 0; @@ -255,6 +291,15 @@ public override string ToString() return NBitcoin.DataEncoders.Encoders.Hex.EncodeData(ToBytes()); } + /// + /// Create a cached Golomb-Rice stream reader. + /// + /// A new cached Golomb-Rice stream reader instance + public CachedGRCodedStreamReader GetNewGRStreamReader() + { + return new CachedGRCodedStreamReader(new BitStream(Data), P, 0); + } + internal static ulong FastReduction(ulong value, ulong nhi, ulong nlo) { // First, we'll spit the item we need to reduce into its higher and lower bits.