Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions src/Base58Encoding.Tests/Base58DecodeFast.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ArgumentException>(() => Base58.DecodeBitcoin64Fast(input));
}

[Theory]
[InlineData("invalid")] // Invalid character 'l'
[InlineData("0chars")] // Invalid character '0'

public void Decode32ast_WithInvalidInput_ReturnsNull(string input)
{
Assert.Throws<ArgumentException>(() => Base58.DecodeBitcoin32Fast(input));
}

[Fact]
Expand Down
36 changes: 36 additions & 0 deletions src/Base58Encoding.Tests/SimpleLeadingZerosTest.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Runtime.Intrinsics;

namespace Base58Encoding.Tests;

public class SimpleLeadingZerosTest
Expand Down Expand Up @@ -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<byte>.Count * 4, processed); // Vector256 used 4 times
}
}
20 changes: 0 additions & 20 deletions src/Base58Encoding/Base58.CountLeading.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,26 +45,6 @@ internal static int CountLeadingZerosSimd(ReadOnlySpan<byte> data, out int proce
length -= Vector256<byte>.Count;
}
}
else if (Vector128.IsHardwareAccelerated && length >= Vector128<byte>.Count)
{
var zeroVector = Vector128<byte>.Zero;

while (length >= Vector128<byte>.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<byte>.Count;
return count + BitOperations.TrailingZeroCount(~mask);
}

count += Vector128<byte>.Count;
length -= Vector128<byte>.Count;
}
}

processed = count;
return count;
Expand Down
22 changes: 13 additions & 9 deletions src/Base58Encoding/Base58.Decode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,16 +114,16 @@ internal byte[] DecodeGeneric(ReadOnlySpan<char> 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<ulong> intermediate = stackalloc ulong[Base58BitcoinTables.IntermediateSz32]; // 9 elements
Span<ulong> intermediate = stackalloc ulong[Base58BitcoinTables.IntermediateSz32];

for (int i = 0; i < Base58BitcoinTables.IntermediateSz32; i++)
{
Expand All @@ -135,7 +135,7 @@ internal byte[] DecodeGeneric(ReadOnlySpan<char> encoded)
}

// Convert to overcomplete base 2^32 using decode table
Span<ulong> binary = stackalloc ulong[Base58BitcoinTables.BinarySz32]; // 8 elements
Span<ulong> binary = stackalloc ulong[Base58BitcoinTables.BinarySz32];

for (int j = 0; j < Base58BitcoinTables.BinarySz32; j++)
{
Expand Down Expand Up @@ -183,6 +183,8 @@ internal byte[] DecodeGeneric(ReadOnlySpan<char> 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
Expand All @@ -194,7 +196,7 @@ internal byte[] DecodeGeneric(ReadOnlySpan<char> encoded)
int charCount = encoded.Length;

// Convert to raw base58 digits with validation + conversion in one pass
Span<byte> rawBase58 = stackalloc byte[Base58BitcoinTables.Raw58Sz64]; // 90 bytes
Span<byte> rawBase58 = stackalloc byte[Base58BitcoinTables.Raw58Sz64];
var bitcoinDecodeTable = Base58Alphabet.Bitcoin.DecodeTable.Span;

// Prepend zeros to make exactly Raw58Sz64 characters
Expand All @@ -208,16 +210,16 @@ internal byte[] DecodeGeneric(ReadOnlySpan<char> 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<ulong> intermediate = stackalloc ulong[Base58BitcoinTables.IntermediateSz64]; // 18 elements
Span<ulong> intermediate = stackalloc ulong[Base58BitcoinTables.IntermediateSz64];

for (int i = 0; i < Base58BitcoinTables.IntermediateSz64; i++)
{
Expand All @@ -229,7 +231,7 @@ internal byte[] DecodeGeneric(ReadOnlySpan<char> encoded)
}

// Convert to overcomplete base 2^32 using decode table
Span<ulong> binary = stackalloc ulong[Base58BitcoinTables.BinarySz64]; // 16 elements
Span<ulong> binary = stackalloc ulong[Base58BitcoinTables.BinarySz64];

for (int j = 0; j < Base58BitcoinTables.BinarySz64; j++)
{
Expand Down Expand Up @@ -277,6 +279,8 @@ internal byte[] DecodeGeneric(ReadOnlySpan<char> 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
Expand Down
12 changes: 0 additions & 12 deletions src/Base58Encoding/Base58.Encode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,6 @@ internal string EncodeGeneric(ReadOnlySpan<byte> data)

internal static string EncodeBitcoin32Fast(ReadOnlySpan<byte> data)
{
// Count leading zeros (needed for final output)
int inLeadingZeros = CountLeadingZeros(data);

if (inLeadingZeros == data.Length)
Expand Down Expand Up @@ -140,8 +139,6 @@ internal static string EncodeBitcoin32Fast(ReadOnlySpan<byte> 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
Expand All @@ -156,12 +153,9 @@ internal static string EncodeBitcoin32Fast(ReadOnlySpan<byte> 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');
Expand All @@ -181,7 +175,6 @@ internal static string EncodeBitcoin32Fast(ReadOnlySpan<byte> data)

private static string EncodeBitcoin64Fast(ReadOnlySpan<byte> data)
{
// Count leading zeros (needed for final output)
int inLeadingZeros = CountLeadingZeros(data);

if (inLeadingZeros == data.Length)
Expand Down Expand Up @@ -242,7 +235,6 @@ private static string EncodeBitcoin64Fast(ReadOnlySpan<byte> 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,
Expand All @@ -256,17 +248,13 @@ private static string EncodeBitcoin64Fast(ReadOnlySpan<byte> 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');
Expand Down