diff --git a/src/Neo/Cryptography/Murmur32.cs b/src/Neo/Cryptography/Murmur32.cs index aa7eb98afa..92d2b6f255 100644 --- a/src/Neo/Cryptography/Murmur32.cs +++ b/src/Neo/Cryptography/Murmur32.cs @@ -11,6 +11,7 @@ using System; using System.Buffers.Binary; +using System.IO.Hashing; using System.Runtime.CompilerServices; namespace Neo.Cryptography @@ -19,7 +20,7 @@ namespace Neo.Cryptography /// Computes the murmur hash for the input data. /// Murmur32 is a non-cryptographic hash function. /// - public sealed class Murmur32 + public sealed class Murmur32 : NonCryptographicHashAlgorithm { private const uint c1 = 0xcc9e2d51; private const uint c2 = 0x1b873593; @@ -32,6 +33,9 @@ public sealed class Murmur32 private uint _hash; private int _length; + private uint _tail; + private int _tailLength; + public const int HashSizeInBits = 32; [Obsolete("Use HashSizeInBits")] @@ -41,46 +45,73 @@ public sealed class Murmur32 /// Initializes a new instance of the class with the specified seed. /// /// The seed to be used. - public Murmur32(uint seed) + public Murmur32(uint seed) : base(HashSizeInBits / 8) { _seed = seed; + Reset(); } - /// - /// Append data to murmur computation - /// - /// Source - private void Append(ReadOnlySpan source) + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Reset() + { + _hash = _seed; + _length = 0; + _tailLength = 0; + _tail = 0; + } + + /// + public override void Append(ReadOnlySpan source) { _length += source.Length; + if (_tailLength > 0) + { + var remaining = Math.Min(4 - _tailLength, source.Length); + _tail ^= ReadUInt32(source[..remaining]) << (_tailLength * 8); + _tailLength += remaining; + if (_tailLength == 4) + { + Mix(_tail); + _tailLength = 0; + _tail = 0; + } + source = source[remaining..]; + } +#if NET7_0_OR_GREATER + for (; source.Length >= 16; source = source[16..]) + { + var k = BinaryPrimitives.ReadUInt128LittleEndian(source); + Mix((uint)k); + Mix((uint)(k >> 32)); + Mix((uint)(k >> 64)); + Mix((uint)(k >> 96)); + } +#endif + for (; source.Length >= 4; source = source[4..]) { - var k = BinaryPrimitives.ReadUInt32LittleEndian(source); - k *= c1; - k = Helper.RotateLeft(k, r1); - k *= c2; - _hash ^= k; - _hash = Helper.RotateLeft(_hash, r2); - _hash = _hash * m + n; + Mix(BinaryPrimitives.ReadUInt32LittleEndian(source)); } + if (source.Length > 0) { - uint remainingBytes = 0; - switch (source.Length) - { - case 3: remainingBytes ^= (uint)source[2] << 16; goto case 2; - case 2: remainingBytes ^= (uint)source[1] << 8; goto case 1; - case 1: remainingBytes ^= source[0]; break; - } - remainingBytes *= c1; - remainingBytes = Helper.RotateLeft(remainingBytes, r1); - remainingBytes *= c2; - _hash ^= remainingBytes; + _tail = ReadUInt32(source); + _tailLength = source.Length; } } - private uint GetCurrentHashUInt32() + /// + protected override void GetCurrentHashCore(Span destination) + { + BinaryPrimitives.WriteUInt32LittleEndian(destination, GetCurrentHashUInt32()); + } + + internal uint GetCurrentHashUInt32() { + if (_tailLength > 0) + _hash ^= Helper.RotateLeft(_tail * c1, r1) * c2; + var state = _hash ^ (uint)_length; state ^= state >> 16; state *= 0x85ebca6b; @@ -91,10 +122,27 @@ private uint GetCurrentHashUInt32() } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void Initialize() + private void Mix(uint k) { - _hash = _seed; - _length = 0; + k *= c1; + k = Helper.RotateLeft(k, r1); + k *= c2; + _hash ^= k; + _hash = Helper.RotateLeft(_hash, r2); + _hash = _hash * m + n; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint ReadUInt32(ReadOnlySpan source) + { + uint value = 0; + switch (source.Length) + { + case 3: value ^= (uint)source[2] << 16; goto case 2; + case 2: value ^= (uint)source[1] << 8; goto case 1; + case 1: value ^= source[0]; break; + } + return value; } /// @@ -110,14 +158,14 @@ public byte[] ComputeHash(ReadOnlySpan data) } /// - /// Computes the murmur hash for the input data and resets the state. + /// Resets the state and computes the murmur hash for the input data. /// /// The input to compute the hash code for. /// The computed hash code in uint. [MethodImpl(MethodImplOptions.AggressiveInlining)] public uint ComputeHashUInt32(ReadOnlySpan data) { - Initialize(); + Reset(); Append(data); return GetCurrentHashUInt32(); } diff --git a/tests/Neo.UnitTests/Cryptography/UT_Murmur32.cs b/tests/Neo.UnitTests/Cryptography/UT_Murmur32.cs index 11797218fb..241a5845b8 100644 --- a/tests/Neo.UnitTests/Cryptography/UT_Murmur32.cs +++ b/tests/Neo.UnitTests/Cryptography/UT_Murmur32.cs @@ -11,6 +11,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Neo.Cryptography; +using System; using System.Buffers.Binary; namespace Neo.UnitTests.Cryptography @@ -47,6 +48,51 @@ public void TestComputeHashUInt32() var murmur3 = new Murmur32(10u); var hash = murmur3.ComputeHashUInt32("hello worldhello world"u8.ToArray()); Assert.AreEqual(60539726u, hash); + + hash = murmur3.ComputeHashUInt32("he"u8.ToArray()); + Assert.AreEqual(972873329u, hash); + } + + [TestMethod] + public void TestAppend() + { + var murmur3 = new Murmur32(10u); + murmur3.Append("h"u8.ToArray()); + murmur3.Append("e"u8.ToArray()); + Assert.AreEqual(972873329u, murmur3.GetCurrentHashUInt32()); + + murmur3.Reset(); + murmur3.Append("hello world"u8.ToArray()); + murmur3.Append("hello world"u8.ToArray()); + Assert.AreEqual(60539726u, murmur3.GetCurrentHashUInt32()); + + murmur3.Reset(); + murmur3.Append("hello worldh"u8.ToArray()); + murmur3.Append("ello world"u8.ToArray()); + Assert.AreEqual(60539726u, murmur3.GetCurrentHashUInt32()); + + murmur3.Reset(); + murmur3.Append("hello worldhello world"u8.ToArray()); + murmur3.Append(""u8.ToArray()); + Assert.AreEqual(60539726u, murmur3.GetCurrentHashUInt32()); + + murmur3.Reset(); + murmur3.Append(""u8.ToArray()); + murmur3.Append("hello worldhello world"u8.ToArray()); + Assert.AreEqual(60539726u, murmur3.GetCurrentHashUInt32()); + + // random data, random split + var random = new Random(); + var data = new byte[random.Next(1, 2048)]; + random.NextBytes(data); + for (int i = 0; i < 32; i++) + { + var split = random.Next(1, data.Length - 1); + murmur3.Reset(); + murmur3.Append(data.AsSpan(0, split)); + murmur3.Append(data.AsSpan(split)); + Assert.AreEqual(data.Murmur32(10u), murmur3.GetCurrentHashUInt32()); + } } } }