From 142fbbdae48c89f742fbfc4600e330ac3989bd85 Mon Sep 17 00:00:00 2001 From: Nikolay Zdravkov Date: Fri, 9 Jan 2026 02:30:09 +0200 Subject: [PATCH] feat: drop vector128 for counting zeros --- src/Base58Encoding.Tests/Base58DecodeFast.cs | 16 +++++++-- .../SimpleLeadingZerosTest.cs | 36 +++++++++++++++++++ src/Base58Encoding/Base58.CountLeading.cs | 20 ----------- src/Base58Encoding/Base58.Decode.cs | 22 +++++++----- src/Base58Encoding/Base58.Encode.cs | 12 ------- 5 files changed, 62 insertions(+), 44 deletions(-) diff --git a/src/Base58Encoding.Tests/Base58DecodeFast.cs b/src/Base58Encoding.Tests/Base58DecodeFast.cs index 9ccbac7..674fb0c 100644 --- a/src/Base58Encoding.Tests/Base58DecodeFast.cs +++ b/src/Base58Encoding.Tests/Base58DecodeFast.cs @@ -76,10 +76,20 @@ public void Decode64Fast_WithAllZeros_WorksCorrectly() } [Theory] - [InlineData("invalid0chars")] // Invalid character '0' - public void Decode32Fast_WithInvalidInput_ReturnsNull(string input) + [InlineData("invalid")] // Invalid character 'l' + [InlineData("0chars")] // Invalid character '0' + public void Decode64Fast_WithInvalidInput_ReturnsNull(string input) { - Assert.Null(Base58.DecodeBitcoin64Fast(input)); + Assert.Throws(() => Base58.DecodeBitcoin64Fast(input)); + } + + [Theory] + [InlineData("invalid")] // Invalid character 'l' + [InlineData("0chars")] // Invalid character '0' + + public void Decode32ast_WithInvalidInput_ReturnsNull(string input) + { + Assert.Throws(() => Base58.DecodeBitcoin32Fast(input)); } [Fact] diff --git a/src/Base58Encoding.Tests/SimpleLeadingZerosTest.cs b/src/Base58Encoding.Tests/SimpleLeadingZerosTest.cs index 1df5cdc..6904301 100644 --- a/src/Base58Encoding.Tests/SimpleLeadingZerosTest.cs +++ b/src/Base58Encoding.Tests/SimpleLeadingZerosTest.cs @@ -1,3 +1,5 @@ +using System.Runtime.Intrinsics; + namespace Base58Encoding.Tests; public class SimpleLeadingZerosTest @@ -27,4 +29,38 @@ public void BitcoinAddress_CountLeadingZerosMultipleWays_SameResult() Assert.Equal(simdScalarCount, manualCount); } + + [Theory] + [InlineData(5)] + [InlineData(12)] + [InlineData(13)] + [InlineData(23)] + [InlineData(31)] + public void CountLeadingZeros_32Size_ReturnsCorrectNumber(int zerosCount) + { + // Arrange + var data = new byte[32]; + data.AsSpan(0, zerosCount).Fill(0x00); + Random.Shared.NextBytes(data.AsSpan(zerosCount)); + + // Act + var result = Base58.CountLeadingZerosSimd(data, out var processed); + + Assert.Equal(zerosCount, result); + Assert.Equal(data.Length, processed); + } + + [Fact] + public void CountLeadingZeros_512Size_ReturnsCorrectNumber() + { + // Arrange + var zerosCount = 123; + var data = new byte[512]; + data.AsSpan(0, zerosCount).Fill(0x00); + Random.Shared.NextBytes(data.AsSpan(zerosCount)); + // Act + var result = Base58.CountLeadingZerosSimd(data, out var processed); + Assert.Equal(zerosCount, result); + Assert.Equal(Vector256.Count * 4, processed); // Vector256 used 4 times + } } diff --git a/src/Base58Encoding/Base58.CountLeading.cs b/src/Base58Encoding/Base58.CountLeading.cs index 11001bf..d5b5d7e 100644 --- a/src/Base58Encoding/Base58.CountLeading.cs +++ b/src/Base58Encoding/Base58.CountLeading.cs @@ -45,26 +45,6 @@ internal static int CountLeadingZerosSimd(ReadOnlySpan data, out int proce length -= Vector256.Count; } } - else if (Vector128.IsHardwareAccelerated && length >= Vector128.Count) - { - var zeroVector = Vector128.Zero; - - while (length >= Vector128.Count) - { - var vector = Vector128.LoadUnsafe(ref searchSpace, (nuint)count); - var comparison = Vector128.Equals(vector, zeroVector); - uint mask = comparison.ExtractMostSignificantBits(); - - if (mask != ushort.MaxValue) - { - processed = count + Vector128.Count; - return count + BitOperations.TrailingZeroCount(~mask); - } - - count += Vector128.Count; - length -= Vector128.Count; - } - } processed = count; return count; diff --git a/src/Base58Encoding/Base58.Decode.cs b/src/Base58Encoding/Base58.Decode.cs index b534926..6268fbf 100644 --- a/src/Base58Encoding/Base58.Decode.cs +++ b/src/Base58Encoding/Base58.Decode.cs @@ -114,16 +114,16 @@ internal byte[] DecodeGeneric(ReadOnlySpan encoded) else { char c = encoded[j - prepend0]; - // Validate + convert using Bitcoin decode table (return null for invalid chars) + // Validate + convert using Bitcoin decode table if (c >= 128 || bitcoinDecodeTable[c] == 255) - return null; + ThrowHelper.ThrowInvalidCharacter(c); rawBase58[j] = bitcoinDecodeTable[c]; } } // Convert to intermediate format (base 58^5) - Span intermediate = stackalloc ulong[Base58BitcoinTables.IntermediateSz32]; // 9 elements + Span intermediate = stackalloc ulong[Base58BitcoinTables.IntermediateSz32]; for (int i = 0; i < Base58BitcoinTables.IntermediateSz32; i++) { @@ -135,7 +135,7 @@ internal byte[] DecodeGeneric(ReadOnlySpan encoded) } // Convert to overcomplete base 2^32 using decode table - Span binary = stackalloc ulong[Base58BitcoinTables.BinarySz32]; // 8 elements + Span binary = stackalloc ulong[Base58BitcoinTables.BinarySz32]; for (int j = 0; j < Base58BitcoinTables.BinarySz32; j++) { @@ -183,6 +183,8 @@ internal byte[] DecodeGeneric(ReadOnlySpan encoded) } // Leading zeros in output must match leading '1's in input + // might be edge case since base58 of 32bytes can be between 32 and 44 characters. + // will be handled by generic decoder if lengths don't match if (outputLeadingZeros != inputLeadingOnes) return null; // Return the full 32 bytes - the result should always be 32 bytes for 32-byte decode @@ -194,7 +196,7 @@ internal byte[] DecodeGeneric(ReadOnlySpan encoded) int charCount = encoded.Length; // Convert to raw base58 digits with validation + conversion in one pass - Span rawBase58 = stackalloc byte[Base58BitcoinTables.Raw58Sz64]; // 90 bytes + Span rawBase58 = stackalloc byte[Base58BitcoinTables.Raw58Sz64]; var bitcoinDecodeTable = Base58Alphabet.Bitcoin.DecodeTable.Span; // Prepend zeros to make exactly Raw58Sz64 characters @@ -208,16 +210,16 @@ internal byte[] DecodeGeneric(ReadOnlySpan encoded) else { char c = encoded[j - prepend0]; - // Validate + convert using Bitcoin decode table (return null for invalid chars) + // Validate + convert using Bitcoin decode table if (c >= 128 || bitcoinDecodeTable[c] == 255) - return null; + ThrowHelper.ThrowInvalidCharacter(c); rawBase58[j] = bitcoinDecodeTable[c]; } } // Convert to intermediate format (base 58^5) - Span intermediate = stackalloc ulong[Base58BitcoinTables.IntermediateSz64]; // 18 elements + Span intermediate = stackalloc ulong[Base58BitcoinTables.IntermediateSz64]; for (int i = 0; i < Base58BitcoinTables.IntermediateSz64; i++) { @@ -229,7 +231,7 @@ internal byte[] DecodeGeneric(ReadOnlySpan encoded) } // Convert to overcomplete base 2^32 using decode table - Span binary = stackalloc ulong[Base58BitcoinTables.BinarySz64]; // 16 elements + Span binary = stackalloc ulong[Base58BitcoinTables.BinarySz64]; for (int j = 0; j < Base58BitcoinTables.BinarySz64; j++) { @@ -277,6 +279,8 @@ internal byte[] DecodeGeneric(ReadOnlySpan encoded) } // Leading zeros in output must match leading '1's in input + // might be edge case since base58 of 64bytes can be between 64 and 88 characters. + // will be handled by generic decoder if lengths don't match if (outputLeadingZeros != inputLeadingOnes) return null; // Return the full 64 bytes - the result should always be 64 bytes for 64-byte decode diff --git a/src/Base58Encoding/Base58.Encode.cs b/src/Base58Encoding/Base58.Encode.cs index 12d6f20..9492d95 100644 --- a/src/Base58Encoding/Base58.Encode.cs +++ b/src/Base58Encoding/Base58.Encode.cs @@ -93,7 +93,6 @@ internal string EncodeGeneric(ReadOnlySpan data) internal static string EncodeBitcoin32Fast(ReadOnlySpan data) { - // Count leading zeros (needed for final output) int inLeadingZeros = CountLeadingZeros(data); if (inLeadingZeros == data.Length) @@ -140,8 +139,6 @@ internal static string EncodeBitcoin32Fast(ReadOnlySpan data) rawBase58[5 * i + 2] = (byte)((v / 3364U) % 58U); rawBase58[5 * i + 1] = (byte)((v / 195112U) % 58U); rawBase58[5 * i + 0] = (byte)(v / 11316496U); - - // Continue processing all values } // Count leading zeros in raw output @@ -156,12 +153,9 @@ internal static string EncodeBitcoin32Fast(ReadOnlySpan data) Debug.Assert(skip >= 0, "rawLeadingZeros should always be >= inLeadingZeros by Base58 math"); int outputLength = Base58BitcoinTables.Raw58Sz32 - skip; - // Create state for string.Create var state = new EncodeFastState(rawBase58, inLeadingZeros, rawLeadingZeros, outputLength); - return string.Create(outputLength, state, static (span, state) => { - // Fill leading '1's for input leading zeros if (state.InLeadingZeros > 0) { span[..state.InLeadingZeros].Fill('1'); @@ -181,7 +175,6 @@ internal static string EncodeBitcoin32Fast(ReadOnlySpan data) private static string EncodeBitcoin64Fast(ReadOnlySpan data) { - // Count leading zeros (needed for final output) int inLeadingZeros = CountLeadingZeros(data); if (inLeadingZeros == data.Length) @@ -242,7 +235,6 @@ private static string EncodeBitcoin64Fast(ReadOnlySpan data) rawBase58[5 * i + 1] = (byte)((v / 195112U) % 58U); rawBase58[5 * i + 0] = (byte)(v / 11316496U); - // Debug.Assert - ensure all values are valid Base58 digits (algorithm correctness check) Debug.Assert(rawBase58[5 * i + 0] < 58 && rawBase58[5 * i + 1] < 58 && rawBase58[5 * i + 2] < 58 && rawBase58[5 * i + 3] < 58 && rawBase58[5 * i + 4] < 58, @@ -256,17 +248,13 @@ private static string EncodeBitcoin64Fast(ReadOnlySpan data) if (rawBase58[rawLeadingZeros] != 0) break; } - // Calculate skip and final length int skip = rawLeadingZeros - inLeadingZeros; Debug.Assert(skip >= 0, "rawLeadingZeros should always be >= inLeadingZeros by Base58 math"); int outputLength = Base58BitcoinTables.Raw58Sz64 - skip; - // Create state for string.Create var state = new EncodeFastState(rawBase58, inLeadingZeros, rawLeadingZeros, outputLength); - return string.Create(outputLength, state, static (span, state) => { - // Fill leading '1's for input leading zeros if (state.InLeadingZeros > 0) { span[..state.InLeadingZeros].Fill('1');