diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..bc2dd9e
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,18 @@
+root = true
+
+[*]
+indent_style = space
+indent_size = 4
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.cs]
+csharp_new_line_before_open_brace = all
+csharp_new_line_before_else = true
+csharp_new_line_before_catch = true
+csharp_new_line_before_finally = true
+
+dotnet_separate_import_directive_groups = true
+dotnet_sort_system_usings_first = true
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..92d168b
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,3 @@
+* text=auto
+
+*.cs text diff=csharp
\ No newline at end of file
diff --git a/.github/workflows/publish-nuget.yml b/.github/workflows/publish-nuget.yml
index 787c307..ce42848 100644
--- a/.github/workflows/publish-nuget.yml
+++ b/.github/workflows/publish-nuget.yml
@@ -22,6 +22,9 @@ on:
required: false
type: string
+permissions:
+ id-token: write # required for GitHub OIDC
+
jobs:
build:
runs-on: ubuntu-latest
@@ -42,7 +45,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
- dotnet-version: 9.0.x
+ dotnet-version: 10.0.x
- name: Restore dependencies
run: dotnet restore src/Base58Encoding.slnx
@@ -71,6 +74,7 @@ jobs:
if: startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && github.event.inputs.version != '')
permissions:
contents: write
+ id-token: write # enable GitHub OIDC token issuance for this job
steps:
- name: Download artifacts
@@ -80,12 +84,18 @@ jobs:
path: ./artifacts
- name: Setup .NET
- uses: actions/setup-dotnet@v4
+ uses: actions/setup-dotnet@v5
+ with:
+ dotnet-version: 10.0.x
+
+ - name: NuGet login (OIDC → temp API key)
+ uses: NuGet/login@v1
+ id: login
with:
- dotnet-version: 9.0.x
+ user: ${{ secrets.NUGET_USER }}
- name: Publish to NuGet
- run: dotnet nuget push ./artifacts/*.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }} --skip-duplicate
+ run: dotnet nuget push ./artifacts/*.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ steps.login.outputs.NUGET_API_KEY }} --skip-duplicate
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
diff --git a/README.md b/README.md
index 9eb7a54..5dbb280 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,14 @@
# Base58 Encoding Library
-A .NET 9.0 Base58 encoding and decoding library with support for multiple alphabet variants.
+A .NET 10.0 Base58 encoding and decoding library with support for multiple alphabet variants.
## Features
-- **Multiple Alphabets**: Built-in support for Bitcoin(IFPS/Sui), Ripple, and Flickr alphabets
+- **Multiple Alphabets**: Built-in support for Bitcoin(IFPS/Sui/Solana), Ripple, and Flickr alphabets
- **Memory Efficient**: Uses stackalloc operations when possible to minimize allocations
- **Type Safe**: Leverages ReadOnlySpan and ReadOnlyMemory for safe memory operations
- **Intrinsics**: Uses SIMD `Vector128/Vector256` and unrolled loop for counting leading zeros
+- **Optimized Hot Paths**: Fast fixed-length encode/decode for 32-byte and 64-byte inputs using Firedancer-like optimizations
## Usage
@@ -24,50 +25,69 @@ byte[] decoded = Base58.Bitcoin.Decode(encoded);
// Ripple / Flickr
Base58.Ripple.Encode(data);
Base58.Flickr.Encode(data);
+```
+
+## Performance
+
+The library automatically uses optimized fast paths for common fixed-size inputs:
+- **32-byte inputs** (Bitcoin/Solana addresses, SHA-256 hashes): 8.5x faster encoding
+- **64-byte inputs** (SHA-512 hashes): Similar performance improvements
+
+These optimizations are based on Firedancer's specialized Base58 algorithms and are transparent to the user. Unlike Firedancer however, we fallback to the generic approach in case of edge-cases.
+
+**Algorithm Details:**
+- Uses **Mixed Radix Conversion (MRC)** with intermediate base 58^5 representation
+- Precomputed multiplication tables replace expensive division operations
+- Converts binary data to base 58^5 limbs, then to raw base58 digits
+- Matrix multiplication approach processes 5 base58 digits simultaneously
+- Separate encode/decode tables for 32-byte and 64-byte fixed sizes
+- Achieves ~2.5x speedup through table-based optimizations vs iterative division
+
+**References:**
+- [Firedancer C implementation](https://github.com/firedancer-io/firedancer/tree/main/src/ballet/base58)
-// Custom
-new Base58(Base58Alphabet.Custom(""));
```
## Benchmarks
```
-BenchmarkDotNet v0.15.2, Windows 11 (10.0.26100.4946/24H2/2024Update/HudsonValley)
+BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.7462/25H2/2025Update/HudsonValley2)
13th Gen Intel Core i7-13700KF 3.40GHz, 1 CPU, 24 logical and 16 physical cores
-.NET SDK 9.0.304
- [Host] : .NET 9.0.8 (9.0.825.36511), X64 RyuJIT AVX2
- .NET 9.0 : .NET 9.0.8 (9.0.825.36511), X64 RyuJIT AVX2
+.NET SDK 10.0.101
+ [Host] : .NET 10.0.1 (10.0.1, 10.0.125.57005), X64 RyuJIT x86-64-v3
+ DefaultJob : .NET 10.0.1 (10.0.1, 10.0.125.57005), X64 RyuJIT x86-64-v3
+
+Job=DefaultJob
-Job=.NET 9.0 Runtime=.NET 9.0
```
-| Method | VectorType | Mean | Error | StdDev | Median |
-|--------------------------- |--------------- |------------:|----------:|----------:|------------:|
-| **'Our Base58 Encode'** | **BitcoinAddress** | **519.2 ns** | **1.99 ns** | **1.76 ns** | **519.2 ns** |
-| 'SimpleBase Base58 Encode' | BitcoinAddress | 770.1 ns | 4.93 ns | 4.61 ns | 769.8 ns |
-| 'Our Base58 Decode' | BitcoinAddress | 187.5 ns | 2.06 ns | 1.93 ns | 187.8 ns |
-| 'SimpleBase Base58 Decode' | BitcoinAddress | 502.1 ns | 6.95 ns | 5.43 ns | 502.4 ns |
-| | | | | | |
-| **'Our Base58 Encode'** | **SolanaAddress** | **1,375.9 ns** | **36.83 ns** | **108.59 ns** | **1,410.7 ns** |
-| 'SimpleBase Base58 Encode' | SolanaAddress | 2,546.5 ns | 112.91 ns | 329.36 ns | 2,661.5 ns |
-| 'Our Base58 Decode' | SolanaAddress | 536.5 ns | 24.89 ns | 73.39 ns | 564.3 ns |
-| 'SimpleBase Base58 Decode' | SolanaAddress | 1,210.2 ns | 31.14 ns | 90.33 ns | 1,236.8 ns |
-| | | | | | |
-| **'Our Base58 Encode'** | **SolanaTx** | **4,185.4 ns** | **48.51 ns** | **37.87 ns** | **4,173.0 ns** |
-| 'SimpleBase Base58 Encode' | SolanaTx | 10,844.0 ns | 337.01 ns | 988.40 ns | 11,158.2 ns |
-| 'Our Base58 Decode' | SolanaTx | 2,159.0 ns | 116.99 ns | 344.95 ns | 2,294.0 ns |
-| 'SimpleBase Base58 Decode' | SolanaTx | 5,357.9 ns | 177.28 ns | 494.18 ns | 5,506.1 ns |
-| | | | | | |
-| **'Our Base58 Encode'** | **IPFSHash** | **1,285.9 ns** | **80.51 ns** | **237.37 ns** | **1,081.6 ns** |
-| 'SimpleBase Base58 Encode' | IPFSHash | 1,654.3 ns | 4.14 ns | 3.88 ns | 1,653.9 ns |
-| 'Our Base58 Decode' | IPFSHash | 347.0 ns | 1.19 ns | 0.99 ns | 347.0 ns |
-| 'SimpleBase Base58 Decode' | IPFSHash | 883.4 ns | 16.93 ns | 15.01 ns | 883.2 ns |
-| | | | | | |
-| **'Our Base58 Encode'** | **MoneroAddress** | **4,907.0 ns** | **13.36 ns** | **11.84 ns** | **4,907.9 ns** |
-| 'SimpleBase Base58 Encode' | MoneroAddress | 8,998.7 ns | 25.07 ns | 20.94 ns | 8,998.8 ns |
-| 'Our Base58 Decode' | MoneroAddress | 1,367.1 ns | 4.38 ns | 3.89 ns | 1,366.5 ns |
-| 'SimpleBase Base58 Decode' | MoneroAddress | 3,809.8 ns | 59.58 ns | 55.73 ns | 3,797.3 ns |
+| Method | VectorType | Mean | Ratio | Gen0 | Allocated | Alloc Ratio |
+|--------------------------- |--------------- |------------:|------:|-------:|----------:|------------:|
+| **'Our Base58 Encode'** | **BitcoinAddress** | **537.07 ns** | **1.00** | **0.0057** | **96 B** | **1.00** |
+| 'SimpleBase Base58 Encode' | BitcoinAddress | 782.31 ns | 1.46 | 0.0057 | 96 B | 1.00 |
+| 'Our Base58 Decode' | BitcoinAddress | 168.95 ns | 0.31 | 0.0033 | 56 B | 0.58 |
+| 'SimpleBase Base58 Decode' | BitcoinAddress | 352.63 ns | 0.66 | 0.0033 | 56 B | 0.58 |
+| | | | | | | |
+| **'Our Base58 Encode'** | **SolanaAddress** | **93.41 ns** | **1.00** | **0.0070** | **112 B** | **1.00** |
+| 'SimpleBase Base58 Encode' | SolanaAddress | 1,430.37 ns | 15.31 | 0.0057 | 112 B | 1.00 |
+| 'Our Base58 Decode' | SolanaAddress | 181.71 ns | 1.95 | 0.0035 | 56 B | 0.50 |
+| 'SimpleBase Base58 Decode' | SolanaAddress | 837.03 ns | 8.96 | 0.0019 | 56 B | 0.50 |
+| | | | | | | |
+| **'Our Base58 Encode'** | **SolanaTx** | **252.31 ns** | **1.00** | **0.0124** | **200 B** | **1.00** |
+| 'SimpleBase Base58 Encode' | SolanaTx | 7,247.09 ns | 28.73 | 0.0076 | 200 B | 1.00 |
+| 'Our Base58 Decode' | SolanaTx | 178.05 ns | 0.71 | 0.0055 | 88 B | 0.44 |
+| 'SimpleBase Base58 Decode' | SolanaTx | 2,379.54 ns | 9.43 | 0.0038 | 88 B | 0.44 |
+| | | | | | | |
+| **'Our Base58 Encode'** | **IPFSHash** | **1,096.58 ns** | **1.00** | **0.0076** | **120 B** | **1.00** |
+| 'SimpleBase Base58 Encode' | IPFSHash | 1,644.83 ns | 1.50 | 0.0076 | 120 B | 1.00 |
+| 'Our Base58 Decode' | IPFSHash | 287.87 ns | 0.26 | 0.0038 | 64 B | 0.53 |
+| 'SimpleBase Base58 Decode' | IPFSHash | 643.63 ns | 0.59 | 0.0038 | 64 B | 0.53 |
+| | | | | | | |
+| **'Our Base58 Encode'** | **MoneroAddress** | **4,998.35 ns** | **1.00** | **0.0076** | **216 B** | **1.00** |
+| 'SimpleBase Base58 Encode' | MoneroAddress | 8,585.92 ns | 1.72 | - | 216 B | 1.00 |
+| 'Our Base58 Decode' | MoneroAddress | 1,173.48 ns | 0.23 | 0.0057 | 96 B | 0.44 |
+| 'SimpleBase Base58 Decode' | MoneroAddress | 3,716.38 ns | 0.74 | 0.0038 | 96 B | 0.44 |
## License
-This project is available under the MIT License.
\ No newline at end of file
+This project is available under the MIT License.
diff --git a/src/Base58Encoding.Benchmarks/Base58ComparisonBenchmark.cs b/src/Base58Encoding.Benchmarks/Base58ComparisonBenchmark.cs
index 189b880..f5635ca 100644
--- a/src/Base58Encoding.Benchmarks/Base58ComparisonBenchmark.cs
+++ b/src/Base58Encoding.Benchmarks/Base58ComparisonBenchmark.cs
@@ -1,15 +1,11 @@
+using Base58Encoding.Benchmarks.Common;
+
using BenchmarkDotNet.Attributes;
-using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Diagnosers;
-using BenchmarkDotNet.Jobs;
-using Base58Encoding.Benchmarks.Common;
namespace Base58Encoding.Benchmarks;
-[SimpleJob(RuntimeMoniker.Net90)]
-[SimpleJob(RuntimeMoniker.Net10_0)]
[MemoryDiagnoser]
-[DisassemblyDiagnoser(exportCombinedDisassemblyReport: true)]
[HideColumns("Job", "Error", "StdDev", "Median", "RatioSD")]
public class Base58ComparisonBenchmark
{
@@ -55,4 +51,4 @@ public byte[] Decode_SimpleBase58()
{
return SimpleBase.Base58.Bitcoin.Decode(_base58Encoded);
}
-}
\ No newline at end of file
+}
diff --git a/src/Base58Encoding.Benchmarks/Base58Encoding.Benchmarks.csproj b/src/Base58Encoding.Benchmarks/Base58Encoding.Benchmarks.csproj
index ecdccfb..04268dd 100644
--- a/src/Base58Encoding.Benchmarks/Base58Encoding.Benchmarks.csproj
+++ b/src/Base58Encoding.Benchmarks/Base58Encoding.Benchmarks.csproj
@@ -2,15 +2,15 @@
Exe
- net9.0;net10.0
+ net10.0
enable
enable
true
-
-
+
+
diff --git a/src/Base58Encoding.Benchmarks/BoundsCheckComparisonBenchmark.cs b/src/Base58Encoding.Benchmarks/BoundsCheckComparisonBenchmark.cs
index 9fea631..cc3e2f7 100644
--- a/src/Base58Encoding.Benchmarks/BoundsCheckComparisonBenchmark.cs
+++ b/src/Base58Encoding.Benchmarks/BoundsCheckComparisonBenchmark.cs
@@ -1,11 +1,12 @@
-using BenchmarkDotNet.Attributes;
-using BenchmarkDotNet.Columns;
-using BenchmarkDotNet.Diagnosers;
-using BenchmarkDotNet.Jobs;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
+
using Base58Encoding.Benchmarks.Common;
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Diagnosers;
+using BenchmarkDotNet.Jobs;
+
namespace Base58Encoding.Benchmarks;
[SimpleJob(RuntimeMoniker.Net90)]
@@ -17,7 +18,6 @@ public class BoundsCheckComparisonBenchmark
private const int Base = 58;
private const int MaxStackallocByte = 512;
private byte[] _testData = null!;
- private Base58 _base58 = null!;
[Params(TestVectors.VectorType.BitcoinAddress, TestVectors.VectorType.SolanaAddress, TestVectors.VectorType.SolanaTx, TestVectors.VectorType.IPFSHash, TestVectors.VectorType.MoneroAddress, TestVectors.VectorType.FlickrTestData, TestVectors.VectorType.RippleTestData)]
public TestVectors.VectorType VectorType { get; set; }
@@ -26,7 +26,6 @@ public class BoundsCheckComparisonBenchmark
public void Setup()
{
_testData = TestVectors.GetVector(VectorType);
- _base58 = Base58.Bitcoin;
}
[Benchmark(Baseline = true)]
@@ -146,4 +145,4 @@ public unsafe byte[] EncodeFixed()
digits.Slice(0, digitCount).CopyTo(result);
return result;
}
-}
\ No newline at end of file
+}
diff --git a/src/Base58Encoding.Benchmarks/Common/TestVectors.cs b/src/Base58Encoding.Benchmarks/Common/TestVectors.cs
index b299fd4..149c44b 100644
--- a/src/Base58Encoding.Benchmarks/Common/TestVectors.cs
+++ b/src/Base58Encoding.Benchmarks/Common/TestVectors.cs
@@ -39,4 +39,4 @@ public static byte[] GetVector(VectorType type)
_ => BitcoinAddress
};
}
-}
\ No newline at end of file
+}
diff --git a/src/Base58Encoding.Benchmarks/CountLeadingCharactersBenchmark.cs b/src/Base58Encoding.Benchmarks/CountLeadingCharactersBenchmark.cs
index 3341af6..4087db0 100644
--- a/src/Base58Encoding.Benchmarks/CountLeadingCharactersBenchmark.cs
+++ b/src/Base58Encoding.Benchmarks/CountLeadingCharactersBenchmark.cs
@@ -1,10 +1,8 @@
-using BenchmarkDotNet.Attributes;
-using BenchmarkDotNet.Columns;
-using BenchmarkDotNet.Jobs;
-using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
-using static System.Net.Mime.MediaTypeNames;
+
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Jobs;
namespace Base58Encoding.Benchmarks;
@@ -128,4 +126,4 @@ internal static int CountLeadingCharacters(ReadOnlySpan text, char target)
return count;
}
-}
\ No newline at end of file
+}
diff --git a/src/Base58Encoding.Benchmarks/CountLeadingZerosBenchmark.cs b/src/Base58Encoding.Benchmarks/CountLeadingZerosBenchmark.cs
index 73ecac0..e0f4073 100644
--- a/src/Base58Encoding.Benchmarks/CountLeadingZerosBenchmark.cs
+++ b/src/Base58Encoding.Benchmarks/CountLeadingZerosBenchmark.cs
@@ -1,12 +1,11 @@
-using BenchmarkDotNet.Attributes;
-using BenchmarkDotNet.Columns;
-using BenchmarkDotNet.Jobs;
-using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Jobs;
+
namespace Base58Encoding.Benchmarks;
[SimpleJob(RuntimeMoniker.Net90)]
@@ -219,4 +218,4 @@ private static int CountLeadingZerosCombinedImpl(ReadOnlySpan data)
return count + CountLeadingZerosScalarImpl(data.Slice(count));
}
-}
\ No newline at end of file
+}
diff --git a/src/Base58Encoding.Benchmarks/FastVsRegularEncodeBenchmark.cs b/src/Base58Encoding.Benchmarks/FastVsRegularEncodeBenchmark.cs
new file mode 100644
index 0000000..1f9e388
--- /dev/null
+++ b/src/Base58Encoding.Benchmarks/FastVsRegularEncodeBenchmark.cs
@@ -0,0 +1,45 @@
+using BenchmarkDotNet.Attributes;
+
+namespace Base58Encoding.Benchmarks;
+
+[MemoryDiagnoser]
+public class FastVsRegularEncodeBenchmark
+{
+ private byte[] _data = default!;
+ private string _encodedBase58 = default!;
+
+ [GlobalSetup]
+ public void Setup()
+ {
+ _data = new byte[32];
+
+ Random.Shared.NextBytes(_data);
+
+ _encodedBase58 = SimpleBase.Base58.Bitcoin.Encode(_data);
+ }
+
+ [Benchmark]
+ public string RegularEncode()
+ {
+ return Base58.Bitcoin.EncodeGeneric(_data);
+ }
+
+ [Benchmark]
+ public string FastEncode()
+ {
+ return Base58.EncodeBitcoin32Fast(_data);
+ }
+
+
+ [Benchmark]
+ public byte[] RegularDecode()
+ {
+ return Base58.Bitcoin.DecodeGeneric(_encodedBase58);
+ }
+
+ [Benchmark]
+ public byte[] FastDecode()
+ {
+ return Base58.DecodeBitcoin32Fast(_encodedBase58)!;
+ }
+}
diff --git a/src/Base58Encoding.Benchmarks/JaggedVsMultidimensionalArrayBenchmark.cs b/src/Base58Encoding.Benchmarks/JaggedVsMultidimensionalArrayBenchmark.cs
new file mode 100644
index 0000000..48d78f8
--- /dev/null
+++ b/src/Base58Encoding.Benchmarks/JaggedVsMultidimensionalArrayBenchmark.cs
@@ -0,0 +1,381 @@
+using System.Buffers.Binary;
+
+using BenchmarkDotNet.Attributes;
+
+namespace Base58Encoding.Benchmarks;
+
+///
+/// Benchmark comparing jagged arrays (uint[][]) vs multidimensional arrays (uint[,])
+/// for Base58 table lookup performance using the same Fast32 encode/decode logic
+/// Verdict - on encoding both are the same, but on decoding jagged arrays are 5-10% faster.
+///
+[MemoryDiagnoser]
+public class JaggedVsMultidimensionalArrayBenchmark
+{
+ private byte[] _data = default!;
+ private string _encodedBase58 = default!;
+
+ // Jagged arrays (current implementation)
+ private static readonly uint[][] JaggedEncodeTable32 = Base58BitcoinTables.EncodeTable32;
+ private static readonly uint[][] JaggedDecodeTable32 = Base58BitcoinTables.DecodeTable32;
+
+ // Multidimensional arrays (alternative implementation)
+ private static readonly uint[,] MultidimensionalEncodeTable32 = ConvertToMultidimensional(Base58BitcoinTables.EncodeTable32);
+ private static readonly uint[,] MultidimensionalDecodeTable32 = ConvertToMultidimensional(Base58BitcoinTables.DecodeTable32);
+
+ [GlobalSetup]
+ public void Setup()
+ {
+ _data = new byte[32];
+ Random.Shared.NextBytes(_data);
+ _encodedBase58 = SimpleBase.Base58.Bitcoin.Encode(_data);
+ }
+
+ [Benchmark(Baseline = true)]
+ public string EncodeWithJaggedArray()
+ {
+ return EncodeBitcoin32FastJagged(_data);
+ }
+
+ [Benchmark]
+ public string EncodeWithMultidimensionalArray()
+ {
+ return EncodeBitcoin32FastMultidimensional(_data);
+ }
+
+ [Benchmark]
+ public byte[] DecodeWithJaggedArray()
+ {
+ return DecodeBitcoin32FastJagged(_encodedBase58)!;
+ }
+
+ [Benchmark]
+ public byte[] DecodeWithMultidimensionalArray()
+ {
+ return DecodeBitcoin32FastMultidimensional(_encodedBase58)!;
+ }
+
+ private static uint[,] ConvertToMultidimensional(uint[][] jaggedArray)
+ {
+ int rows = jaggedArray.Length;
+ int cols = jaggedArray[0].Length;
+ var result = new uint[rows, cols];
+
+ for (int i = 0; i < rows; i++)
+ {
+ for (int j = 0; j < cols; j++)
+ {
+ result[i, j] = jaggedArray[i][j];
+ }
+ }
+
+ return result;
+ }
+
+ private static string EncodeBitcoin32FastJagged(ReadOnlySpan data)
+ {
+ // Count leading zeros
+ int inLeadingZeros = Base58.CountLeadingZeros(data);
+
+ if (inLeadingZeros == data.Length)
+ {
+ return new string('1', inLeadingZeros);
+ }
+
+ // Convert 32 bytes to 8 uint32 limbs (big-endian)
+ Span binary = stackalloc uint[Base58BitcoinTables.BinarySz32];
+ for (int i = 0; i < Base58BitcoinTables.BinarySz32; i++)
+ {
+ int offset = i * sizeof(uint);
+ binary[i] = BinaryPrimitives.ReadUInt32BigEndian(data.Slice(offset, sizeof(uint)));
+ }
+
+ // Convert to intermediate format (base 58^5) using JAGGED ARRAY
+ Span intermediate = stackalloc ulong[Base58BitcoinTables.IntermediateSz32];
+ intermediate.Clear();
+
+ for (int i = 0; i < Base58BitcoinTables.BinarySz32; i++)
+ {
+ for (int j = 0; j < Base58BitcoinTables.IntermediateSz32 - 1; j++)
+ {
+ intermediate[j + 1] += (ulong)binary[i] * JaggedEncodeTable32[i][j];
+ }
+ }
+
+ // Reduce each term to be less than 58^5
+ for (int i = Base58BitcoinTables.IntermediateSz32 - 1; i > 0; i--)
+ {
+ intermediate[i - 1] += intermediate[i] / Base58BitcoinTables.R1Div;
+ intermediate[i] %= Base58BitcoinTables.R1Div;
+ }
+
+ // Convert intermediate form to raw base58 digits
+ Span rawBase58 = stackalloc byte[Base58BitcoinTables.Raw58Sz32];
+ for (int i = 0; i < Base58BitcoinTables.IntermediateSz32; i++)
+ {
+ uint v = (uint)intermediate[i];
+ rawBase58[5 * i + 4] = (byte)((v / 1U) % 58U);
+ rawBase58[5 * i + 3] = (byte)((v / 58U) % 58U);
+ rawBase58[5 * i + 2] = (byte)((v / 3364U) % 58U);
+ rawBase58[5 * i + 1] = (byte)((v / 195112U) % 58U);
+ rawBase58[5 * i + 0] = (byte)(v / 11316496U);
+ }
+
+ // Count leading zeros in raw output
+ int rawLeadingZeros = 0;
+ for (; rawLeadingZeros < Base58BitcoinTables.Raw58Sz32; rawLeadingZeros++)
+ {
+ if (rawBase58[rawLeadingZeros] != 0) break;
+ }
+
+ // Calculate skip and final length
+ int skip = rawLeadingZeros - inLeadingZeros;
+ int outputLength = Base58BitcoinTables.Raw58Sz32 - skip;
+ var state = new Base58.EncodeFastState(rawBase58, inLeadingZeros, rawLeadingZeros, outputLength);
+ return string.Create(outputLength, state, static (span, state) =>
+ {
+ if (state.InLeadingZeros > 0)
+ {
+ span[..state.InLeadingZeros].Fill('1');
+ }
+
+ var bitcoinChars = Base58BitcoinTables.BitcoinChars;
+ for (int i = 0; i < state.OutputLength - state.InLeadingZeros; i++)
+ {
+ byte digit = state.RawBase58[state.RawLeadingZeros + i];
+ span[state.InLeadingZeros + i] = bitcoinChars[digit];
+ }
+ });
+ }
+
+ private static string EncodeBitcoin32FastMultidimensional(ReadOnlySpan data)
+ {
+ // Count leading zeros
+ int inLeadingZeros = Base58.CountLeadingZeros(data);
+
+ if (inLeadingZeros == data.Length)
+ {
+ return new string('1', inLeadingZeros);
+ }
+
+ // Convert 32 bytes to 8 uint32 limbs (big-endian)
+ Span binary = stackalloc uint[Base58BitcoinTables.BinarySz32];
+ for (int i = 0; i < Base58BitcoinTables.BinarySz32; i++)
+ {
+ int offset = i * sizeof(uint);
+ binary[i] = BinaryPrimitives.ReadUInt32BigEndian(data.Slice(offset, sizeof(uint)));
+ }
+
+ // Convert to intermediate format (base 58^5) using MULTIDIMENSIONAL ARRAY
+ Span intermediate = stackalloc ulong[Base58BitcoinTables.IntermediateSz32];
+ intermediate.Clear();
+
+ for (int i = 0; i < Base58BitcoinTables.BinarySz32; i++)
+ {
+ for (int j = 0; j < Base58BitcoinTables.IntermediateSz32 - 1; j++)
+ {
+ intermediate[j + 1] += (ulong)binary[i] * MultidimensionalEncodeTable32[i, j];
+ }
+ }
+
+ // Reduce each term to be less than 58^5
+ for (int i = Base58BitcoinTables.IntermediateSz32 - 1; i > 0; i--)
+ {
+ intermediate[i - 1] += intermediate[i] / Base58BitcoinTables.R1Div;
+ intermediate[i] %= Base58BitcoinTables.R1Div;
+ }
+
+ // Convert intermediate form to raw base58 digits
+ Span rawBase58 = stackalloc byte[Base58BitcoinTables.Raw58Sz32];
+ for (int i = 0; i < Base58BitcoinTables.IntermediateSz32; i++)
+ {
+ uint v = (uint)intermediate[i];
+ rawBase58[5 * i + 4] = (byte)((v / 1U) % 58U);
+ rawBase58[5 * i + 3] = (byte)((v / 58U) % 58U);
+ rawBase58[5 * i + 2] = (byte)((v / 3364U) % 58U);
+ rawBase58[5 * i + 1] = (byte)((v / 195112U) % 58U);
+ rawBase58[5 * i + 0] = (byte)(v / 11316496U);
+ }
+
+ // Count leading zeros in raw output
+ int rawLeadingZeros = 0;
+ for (; rawLeadingZeros < Base58BitcoinTables.Raw58Sz32; rawLeadingZeros++)
+ {
+ if (rawBase58[rawLeadingZeros] != 0) break;
+ }
+
+ // Calculate skip and final length
+ int skip = rawLeadingZeros - inLeadingZeros;
+ int outputLength = Base58BitcoinTables.Raw58Sz32 - skip;
+
+ var state = new Base58.EncodeFastState(rawBase58, inLeadingZeros, rawLeadingZeros, outputLength);
+ return string.Create(outputLength, state, static (span, state) =>
+ {
+ if (state.InLeadingZeros > 0)
+ {
+ span[..state.InLeadingZeros].Fill('1');
+ }
+
+ var bitcoinChars = Base58BitcoinTables.BitcoinChars;
+ for (int i = 0; i < state.OutputLength - state.InLeadingZeros; i++)
+ {
+ byte digit = state.RawBase58[state.RawLeadingZeros + i];
+ span[state.InLeadingZeros + i] = bitcoinChars[digit];
+ }
+ });
+ }
+
+ private static byte[]? DecodeBitcoin32FastJagged(string encoded)
+ {
+ // Early validation and length check
+ if (encoded.Length > Base58BitcoinTables.Raw58Sz32) return null;
+
+ // Validate characters and create raw array using JAGGED ARRAY lookup
+ Span rawBase58 = stackalloc byte[Base58BitcoinTables.Raw58Sz32];
+ var bitcoinDecodeTable = Base58Alphabet.Bitcoin.DecodeTable.Span;
+
+ int prepend0 = Base58BitcoinTables.Raw58Sz32 - encoded.Length;
+ for (int j = 0; j < Base58BitcoinTables.Raw58Sz32; j++)
+ {
+ if (j < prepend0)
+ {
+ rawBase58[j] = 0;
+ }
+ else
+ {
+ char c = encoded[j - prepend0];
+ if (c >= 128 || bitcoinDecodeTable[c] == 255)
+ return null;
+
+ rawBase58[j] = bitcoinDecodeTable[c];
+ }
+ }
+
+ // Convert to intermediate format
+ Span intermediate = stackalloc ulong[Base58BitcoinTables.IntermediateSz32];
+ for (int i = 0; i < Base58BitcoinTables.IntermediateSz32; i++)
+ {
+ intermediate[i] = (ulong)rawBase58[5 * i + 0] * 11316496UL +
+ (ulong)rawBase58[5 * i + 1] * 195112UL +
+ (ulong)rawBase58[5 * i + 2] * 3364UL +
+ (ulong)rawBase58[5 * i + 3] * 58UL +
+ (ulong)rawBase58[5 * i + 4] * 1UL;
+ }
+
+ // Convert to binary using JAGGED ARRAY
+ Span binary = stackalloc ulong[Base58BitcoinTables.BinarySz32];
+ for (int j = 0; j < Base58BitcoinTables.BinarySz32; j++)
+ {
+ ulong acc = 0UL;
+ for (int i = 0; i < Base58BitcoinTables.IntermediateSz32; i++)
+ {
+ acc += intermediate[i] * JaggedDecodeTable32[i][j];
+ }
+ binary[j] = acc;
+ }
+
+ // Reduce to proper uint32 values
+ for (int i = Base58BitcoinTables.BinarySz32 - 1; i > 0; i--)
+ {
+ binary[i - 1] += binary[i] >> 32;
+ binary[i] &= 0xFFFFFFFFUL;
+ }
+
+ if (binary[0] > 0xFFFFFFFFUL) return null;
+
+ // Convert to output bytes
+ var result = new byte[32];
+ for (int i = 0; i < Base58BitcoinTables.BinarySz32; i++)
+ {
+ BinaryPrimitives.WriteUInt32BigEndian(result.AsSpan(i * 4, 4), (uint)binary[i]);
+ }
+
+ // Validate leading zeros match leading '1's
+ int leadingZeroCnt = 0;
+ for (; leadingZeroCnt < 32; leadingZeroCnt++)
+ {
+ if (result[leadingZeroCnt] != 0) break;
+ if (encoded.Length <= leadingZeroCnt || encoded[leadingZeroCnt] != '1') return null;
+ }
+ if (leadingZeroCnt < encoded.Length && encoded[leadingZeroCnt] == '1') return null;
+
+ return result;
+ }
+
+ private static byte[]? DecodeBitcoin32FastMultidimensional(string encoded)
+ {
+ // Early validation and length check
+ if (encoded.Length > Base58BitcoinTables.Raw58Sz32) return null;
+
+ // Validate characters and create raw array using MULTIDIMENSIONAL ARRAY lookup
+ Span rawBase58 = stackalloc byte[Base58BitcoinTables.Raw58Sz32];
+ var bitcoinDecodeTable = Base58Alphabet.Bitcoin.DecodeTable.Span;
+
+ int prepend0 = Base58BitcoinTables.Raw58Sz32 - encoded.Length;
+ for (int j = 0; j < Base58BitcoinTables.Raw58Sz32; j++)
+ {
+ if (j < prepend0)
+ {
+ rawBase58[j] = 0;
+ }
+ else
+ {
+ char c = encoded[j - prepend0];
+ if (c >= 128 || bitcoinDecodeTable[c] == 255)
+ return null;
+
+ rawBase58[j] = bitcoinDecodeTable[c];
+ }
+ }
+
+ // Convert to intermediate format
+ Span intermediate = stackalloc ulong[Base58BitcoinTables.IntermediateSz32];
+ for (int i = 0; i < Base58BitcoinTables.IntermediateSz32; i++)
+ {
+ intermediate[i] = (ulong)rawBase58[5 * i + 0] * 11316496UL +
+ (ulong)rawBase58[5 * i + 1] * 195112UL +
+ (ulong)rawBase58[5 * i + 2] * 3364UL +
+ (ulong)rawBase58[5 * i + 3] * 58UL +
+ (ulong)rawBase58[5 * i + 4] * 1UL;
+ }
+
+ // Convert to binary using MULTIDIMENSIONAL ARRAY
+ Span binary = stackalloc ulong[Base58BitcoinTables.BinarySz32];
+ for (int j = 0; j < Base58BitcoinTables.BinarySz32; j++)
+ {
+ ulong acc = 0UL;
+ for (int i = 0; i < Base58BitcoinTables.IntermediateSz32; i++)
+ {
+ acc += intermediate[i] * MultidimensionalDecodeTable32[i, j];
+ }
+ binary[j] = acc;
+ }
+
+ // Reduce to proper uint32 values
+ for (int i = Base58BitcoinTables.BinarySz32 - 1; i > 0; i--)
+ {
+ binary[i - 1] += binary[i] >> 32;
+ binary[i] &= 0xFFFFFFFFUL;
+ }
+
+ if (binary[0] > 0xFFFFFFFFUL) return null;
+
+ // Convert to output bytes
+ var result = new byte[32];
+ for (int i = 0; i < Base58BitcoinTables.BinarySz32; i++)
+ {
+ BinaryPrimitives.WriteUInt32BigEndian(result.AsSpan(i * 4, 4), (uint)binary[i]);
+ }
+
+ // Validate leading zeros match leading '1's
+ int leadingZeroCnt = 0;
+ for (; leadingZeroCnt < 32; leadingZeroCnt++)
+ {
+ if (result[leadingZeroCnt] != 0) break;
+ if (encoded.Length <= leadingZeroCnt || encoded[leadingZeroCnt] != '1') return null;
+ }
+ if (leadingZeroCnt < encoded.Length && encoded[leadingZeroCnt] == '1') return null;
+
+ return result;
+ }
+}
diff --git a/src/Base58Encoding.Benchmarks/Program.cs b/src/Base58Encoding.Benchmarks/Program.cs
index e48a046..a534ab4 100644
--- a/src/Base58Encoding.Benchmarks/Program.cs
+++ b/src/Base58Encoding.Benchmarks/Program.cs
@@ -1,6 +1,7 @@
-using BenchmarkDotNet.Running;
using Base58Encoding.Benchmarks;
-//BenchmarkRunner.Run();
-BenchmarkRunner.Run();
+using BenchmarkDotNet.Running;
+
+BenchmarkRunner.Run();
+//BenchmarkRunner.Run();
//BenchmarkRunner.Run();
diff --git a/src/Base58Encoding.Tests/Base58DecodeFast.cs b/src/Base58Encoding.Tests/Base58DecodeFast.cs
new file mode 100644
index 0000000..9ccbac7
--- /dev/null
+++ b/src/Base58Encoding.Tests/Base58DecodeFast.cs
@@ -0,0 +1,263 @@
+namespace Base58Encoding.Tests;
+
+public class Base58DecodeFast
+{
+ [Fact]
+ public void Decode32Fast_WithValidInput_MatchesGeneric()
+ {
+ for (int i = 0; i < 100; i++)
+ {
+ // Arrange
+ var testData = new byte[32];
+ Random.Shared.NextBytes(testData);
+
+ var encoded = Base58.Bitcoin.Encode(testData);
+
+ // Act
+ var fastDecoded = Base58.Bitcoin.Decode(encoded);
+ var genericDecoded = Base58.Bitcoin.DecodeGeneric(encoded);
+
+ // Assert
+ Assert.Equal(genericDecoded, fastDecoded);
+ Assert.Equal(testData, fastDecoded);
+ }
+ }
+
+ [Fact]
+ public void Decode64Fast_WithValidInput_MatchesGeneric()
+ {
+ var random = new Random(42);
+
+ for (int i = 0; i < 100; i++)
+ {
+ // Arrange
+ var testData = new byte[64];
+ random.NextBytes(testData);
+
+ var encoded = Base58.Bitcoin.Encode(testData);
+
+ // Act
+ var fastDecoded = Base58.Bitcoin.Decode(encoded);
+ var genericDecoded = Base58.Bitcoin.DecodeGeneric(encoded);
+
+ // Assert
+ Assert.Equal(genericDecoded, fastDecoded);
+ Assert.Equal(testData, fastDecoded);
+ }
+ }
+
+ [Fact]
+ public void Decode32Fast_WithAllZeros_ReturnsNull()
+ {
+ // Arrange
+ var allZeros = new byte[32];
+ var encoded = SimpleBase.Base58.Bitcoin.Encode(allZeros);
+
+ // Act
+ var decoded = Base58.DecodeBitcoin64Fast(encoded);
+
+ // Assert
+ Assert.Null(decoded);
+ }
+
+ [Fact]
+ public void Decode64Fast_WithAllZeros_WorksCorrectly()
+ {
+ // Arrange
+ var allZeros = new byte[64];
+ var encoded = SimpleBase.Base58.Bitcoin.Encode(allZeros);
+
+ // Act
+ var decoded = Base58.DecodeBitcoin64Fast(encoded);
+
+ // Assert
+ Assert.Equal(allZeros, decoded);
+ Assert.Equal(new string('1', 64), encoded);
+ }
+
+ [Theory]
+ [InlineData("invalid0chars")] // Invalid character '0'
+ public void Decode32Fast_WithInvalidInput_ReturnsNull(string input)
+ {
+ Assert.Null(Base58.DecodeBitcoin64Fast(input));
+ }
+
+ [Fact]
+ public void Decode32Fast_WithLeadingOnes_HandlesCorrectly()
+ {
+ // Arrange - 28 leading zeros + some data
+ var testData = new byte[32];
+ testData[28] = 0xAB;
+ testData[29] = 0xCD;
+ testData[30] = 0xEF;
+ testData[31] = 0x12;
+
+ var encoded = Base58.Bitcoin.Encode(testData);
+
+ // Act
+ var decoded = Base58.Bitcoin.Decode(encoded);
+
+ // Assert
+ Assert.Equal(testData, decoded);
+ Assert.StartsWith(new string('1', 28), encoded);
+ }
+
+ [Fact]
+ public void Decode64Fast_WithLeadingOnes_HandlesCorrectly()
+ {
+ // Arrange - 32 leading zeros + data
+ var testData = new byte[64];
+ for (int i = 32; i < 64; i++)
+ {
+ testData[i] = (byte)(i - 32);
+ }
+
+ var encoded = Base58.Bitcoin.Encode(testData);
+
+ // Act
+ var decoded = Base58.DecodeBitcoin64Fast(encoded);
+
+ // Assert
+ Assert.Equal(testData, decoded);
+ Assert.StartsWith(new string('1', 32), encoded);
+ }
+
+ [Fact]
+ public void Decode32Fast_WithKnownTestVectors_WorksCorrectly()
+ {
+ // Arrange - Create known 32-byte inputs and their expected Base58 outputs
+ var testCases = new[]
+ {
+ new byte[32], // All zeros
+ Enumerable.Repeat((byte)0xFF, 32).ToArray(), // All 255s
+ };
+
+ foreach (var testCase in testCases)
+ {
+ // Act
+ var encoded = SimpleBase.Base58.Bitcoin.Encode(testCase);
+ var decoded = Base58.DecodeBitcoin32Fast(encoded);
+
+ var genericDecoded = Base58.Bitcoin.DecodeGeneric(encoded);
+
+ // Assert
+ Assert.Equal(testCase, decoded);
+
+ // Verify round-trip with generic
+ Assert.Equal(genericDecoded, decoded);
+ }
+ }
+
+ [Fact]
+ public void Decode64Fast_WithKnownTestVectors_WorksCorrectly()
+ {
+ // Arrange - Create known 64-byte inputs
+ var testCases = new[]
+ {
+ new byte[64], // All zeros
+ Enumerable.Repeat((byte)0xFF, 64).ToArray(), // All 255s
+ };
+
+ foreach (var testCase in testCases)
+ {
+ // Act
+ var encoded = Base58.Bitcoin.Encode(testCase);
+ var decoded = Base58.DecodeBitcoin64Fast(encoded);
+
+ // Assert
+ Assert.Equal(testCase, decoded);
+
+ // Verify round-trip with generic
+ var genericDecoded = Base58.Bitcoin.DecodeGeneric(encoded);
+ Assert.Equal(genericDecoded, decoded);
+ }
+ }
+
+ [Theory]
+ [InlineData(0)] // No leading zeros
+ [InlineData(1)] // 1 leading zero
+ [InlineData(5)] // 5 leading zeros
+ [InlineData(16)] // Half the array
+ [InlineData(31)] // Almost all zeros
+ public void Decode32Fast_WithVariousLeadingZeros_HandlesCorrectly(int leadingZeros)
+ {
+ // Arrange
+ var testData = new byte[32];
+ Random.Shared.NextBytes(testData.AsSpan(leadingZeros));
+
+ var encoded = Base58.Bitcoin.Encode(testData);
+
+ // Act
+ var decoded = Base58.Bitcoin.Decode(encoded);
+
+ // Assert
+ Assert.Equal(testData, decoded);
+
+ // Verify leading zeros preservation
+ if (leadingZeros > 0)
+ {
+ Assert.StartsWith(new string('1', leadingZeros), encoded);
+ }
+ }
+
+ [Theory]
+ [InlineData(0)] // No leading zeros
+ [InlineData(1)] // 1 leading zero
+ [InlineData(8)] // 8 leading zeros
+ [InlineData(32)] // Half the array
+ [InlineData(63)] // Almost all zeros
+ public void Decode64Fast_WithVariousLeadingZeros_HandlesCorrectly(int leadingZeros)
+ {
+ // Arrange
+ var testData = new byte[64];
+ Random.Shared.NextBytes(testData.AsSpan(leadingZeros));
+
+ var encoded = Base58.Bitcoin.Encode(testData);
+
+ // Act
+ var decoded = Base58.DecodeBitcoin64Fast(encoded);
+
+ // Assert
+ Assert.Equal(testData, decoded);
+
+ // Verify leading zeros preservation
+ if (leadingZeros > 0)
+ {
+ Assert.StartsWith(new string('1', leadingZeros), encoded);
+ }
+ }
+
+ [Fact]
+ public void Decode32Fast_WithMaximumValues_WorksCorrectly()
+ {
+ // Arrange - Create data that will generate maximum Base58 length
+ var testData = new byte[32];
+ Array.Fill(testData, (byte)0xFF);
+
+ var encoded = Base58.Bitcoin.Encode(testData);
+
+ // Act
+ var decoded = Base58.Bitcoin.Decode(encoded);
+
+ // Assert
+ Assert.Equal(testData, decoded);
+ Assert.InRange(encoded.Length, 43, 44); // Maximum Base58 length for 32 bytes
+ }
+
+ [Fact]
+ public void Decode64Fast_WithMaximumValues_WorksCorrectly()
+ {
+ // Arrange - Create data that will generate maximum Base58 length
+ var testData = new byte[64];
+ Array.Fill(testData, (byte)0xFF);
+
+ var encoded = Base58.Bitcoin.Encode(testData);
+
+ // Act
+ var decoded = Base58.DecodeBitcoin64Fast(encoded);
+
+ // Assert
+ Assert.Equal(testData, decoded);
+ Assert.InRange(encoded.Length, 87, 88); // Maximum Base58 length for 64 bytes
+ }
+}
diff --git a/src/Base58Encoding.Tests/Base58EncodeFast.cs b/src/Base58Encoding.Tests/Base58EncodeFast.cs
new file mode 100644
index 0000000..56a0584
--- /dev/null
+++ b/src/Base58Encoding.Tests/Base58EncodeFast.cs
@@ -0,0 +1,248 @@
+namespace Base58Encoding.Tests;
+
+public class Base58EncodeFast
+{
+
+ [Fact]
+ public void BitcoinAlphabet_EncodeGeneric_Generates_SameResultAsFast32()
+ {
+ Span buffer = stackalloc byte[32];
+ Random.Shared.NextBytes(buffer);
+
+ var genericResult = Base58.Bitcoin.EncodeGeneric(buffer);
+ var fastResult = Base58.EncodeBitcoin32Fast(buffer);
+
+ Assert.Equal(genericResult, fastResult);
+ }
+
+ [Fact]
+ public void Encode32Fast_TableDimensions_AreCorrect()
+ {
+ // Verify table dimensions
+ Assert.Equal(8, Base58BitcoinTables.BinarySz32);
+ Assert.Equal(9, Base58BitcoinTables.IntermediateSz32);
+ Assert.Equal(45, Base58BitcoinTables.Raw58Sz32);
+
+ // Verify table bounds
+ var encodeTable = Base58BitcoinTables.EncodeTable32;
+ Assert.Equal(8, encodeTable.Length); // Should be BinarySz32
+ Assert.Equal(8, encodeTable[0].Length); // Should be IntermediateSz32 - 1
+ }
+
+ [Fact]
+ public void Encode32Fast_WithAllZeros_ReturnsCorrectOnes()
+ {
+ // Arrange
+ var allZeros = new byte[32];
+
+ // Act
+ var result = Base58.Bitcoin.Encode(allZeros);
+
+ // Assert - Should be 32 '1's for 32 zero bytes
+ Assert.Equal(new string('1', 32), result);
+ }
+
+ [Fact]
+ public void Encode32Fast_WithAllOnes_ProducesCorrectResult()
+ {
+ // Arrange
+ var allOnes = Enumerable.Repeat((byte)0xFF, 32).ToArray();
+
+ // Act
+ var fastResult = Base58.Bitcoin.Encode(allOnes);
+ var genericResult = Base58.Bitcoin.EncodeGeneric(allOnes);
+
+ // Assert
+ Assert.Equal(genericResult, fastResult);
+ Assert.NotEmpty(fastResult);
+ }
+
+ [Fact]
+ public void Encode32Fast_WithLeadingZeros_HandlesCorrectly()
+ {
+ // Test one simple case first to isolate the issue
+ var testCase = new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFE, 0xFD, 0xFC }; // 28 leading zeros
+
+ // Act
+ var genericResult = Base58.Bitcoin.EncodeGeneric(testCase);
+ var simpleBase = SimpleBase.Base58.Bitcoin.Encode(testCase);
+ var fastResult = Base58.Bitcoin.Encode(testCase);
+
+ // Assert
+ Assert.Equal(genericResult, fastResult);
+
+ // Verify leading zeros are preserved as '1's
+ int leadingZeros = testCase.TakeWhile(b => b == 0).Count();
+ Assert.StartsWith(new string('1', leadingZeros), fastResult);
+ }
+
+ [Theory]
+ [InlineData(1000)]
+ public void Encode32Fast_WithRandomData_MatchesGeneric(int testCount)
+ {
+ var random = new Random(42); // Fixed seed for reproducibility
+
+ for (int i = 0; i < testCount; i++)
+ {
+ // Arrange
+ var testData = new byte[32];
+ random.NextBytes(testData);
+
+ // Act
+ var fastResult = Base58.Bitcoin.Encode(testData);
+ var genericResult = Base58.Bitcoin.EncodeGeneric(testData);
+
+ // Assert
+ Assert.Equal(genericResult, fastResult);
+
+ // Verify it round-trips correctly
+ var decoded = Base58.Bitcoin.Decode(fastResult);
+ Assert.Equal(testData, decoded);
+ }
+ }
+
+ [Fact]
+ public void Encode32Fast_WithRealBitcoinAddressData_WorksCorrectly()
+ {
+ // Arrange - Real Bitcoin address hash160 (20 bytes padded to 32)
+ var hash160 = new byte[]
+ {
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 12 zero padding
+ 0x76, 0xa9, 0x14, 0x89, 0xab, 0xcd, 0xef, 0xab, 0xba, 0xab, 0xba, 0xab,
+ 0xba, 0xab, 0xba, 0xab, 0xba, 0xab, 0xba, 0xab, 0xba, 0x88, 0xac, 0x00
+ };
+
+ // Act
+ var fastResult = Base58.Bitcoin.Encode(hash160);
+ var genericResult = Base58.Bitcoin.EncodeGeneric(hash160);
+
+ // Assert
+ Assert.Equal(genericResult, fastResult);
+
+ // Should start with 12 '1's due to leading zeros
+ Assert.StartsWith(new string('1', 12), fastResult);
+ }
+
+ [Fact]
+ public void Encode_WithNon32ByteInputs_UsesGenericPath()
+ {
+ var testCases = new[]
+ {
+ new byte[31], // 31 bytes
+ new byte[33], // 33 bytes
+ new byte[20], // 20 bytes (Bitcoin hash160)
+ new byte[64], // 64 bytes (will use generic until we implement 64-byte fast path)
+ };
+
+ foreach (var testCase in testCases)
+ {
+ Random.Shared.NextBytes(testCase);
+
+ // Act
+ var result = Base58.Bitcoin.Encode(testCase);
+
+ // Assert - Should work correctly via generic path
+ var decoded = Base58.Bitcoin.Decode(result);
+ Assert.Equal(testCase, decoded);
+ }
+ }
+
+ [Fact]
+ public void Encode64Fast_TableDimensions_AreCorrect()
+ {
+ // Verify table dimensions
+ Assert.Equal(16, Base58BitcoinTables.BinarySz64);
+ Assert.Equal(18, Base58BitcoinTables.IntermediateSz64);
+ Assert.Equal(90, Base58BitcoinTables.Raw58Sz64);
+
+ // Verify table bounds
+ var encodeTable = Base58BitcoinTables.EncodeTable64;
+ Assert.Equal(16, encodeTable.Length); // Should be BinarySz64
+ Assert.Equal(17, encodeTable[0].Length); // Should be IntermediateSz64 - 1
+ }
+
+ [Fact]
+ public void Encode64Fast_WithAllZeros_ReturnsCorrectOnes()
+ {
+ // Arrange
+ var allZeros = new byte[64];
+
+ // Act
+ var result = Base58.Bitcoin.Encode(allZeros);
+
+ // Assert - Should be 64 '1's for 64 zero bytes
+ Assert.Equal(new string('1', 64), result);
+ }
+
+ [Fact]
+ public void Encode64Fast_WithRandomData_MatchesGeneric()
+ {
+ var random = new Random(42); // Fixed seed for reproducibility
+
+ for (int i = 0; i < 100; i++)
+ {
+ // Arrange
+ var testData = new byte[64];
+ random.NextBytes(testData);
+
+ // Act
+ var fastResult = Base58.Bitcoin.Encode(testData);
+ var genericResult = Base58.Bitcoin.EncodeGeneric(testData);
+
+ // Assert
+ Assert.Equal(genericResult, fastResult);
+
+ // Verify it round-trips correctly
+ var decoded = Base58.Bitcoin.Decode(fastResult);
+ Assert.Equal(testData, decoded);
+ }
+ }
+
+ [Fact]
+ public void Encode64Fast_WithLeadingZeros_HandlesCorrectly()
+ {
+ // Arrange - 64-byte data with leading zeros
+ var testCases = new[]
+ {
+ new byte[64], // All zeros
+ Enumerable.Range(0, 64).Select(i => i < 32 ? (byte)0x00 : (byte)0xFF).ToArray(), // 32 leading zeros
+ };
+
+ foreach (var testCase in testCases)
+ {
+ // Act
+ var fastResult = Base58.Bitcoin.Encode(testCase);
+ var genericResult = Base58.Bitcoin.EncodeGeneric(testCase);
+
+ // Assert
+ Assert.Equal(genericResult, fastResult);
+
+ // Verify leading zeros are preserved as '1's
+ int leadingZeros = testCase.TakeWhile(b => b == 0).Count();
+ Assert.StartsWith(new string('1', leadingZeros), fastResult);
+ }
+ }
+
+ [Fact]
+ public void Encode64Fast_WithSolanaTransactionSignature_WorksCorrectly()
+ {
+ // Arrange - Simulate a 64-byte Solana transaction signature
+ var signatureData = new byte[64];
+ var random = new Random(123);
+ random.NextBytes(signatureData);
+
+ // Act
+ var fastResult = Base58.Bitcoin.Encode(signatureData);
+ var genericResult = Base58.Bitcoin.EncodeGeneric(signatureData);
+
+ // Assert
+ Assert.Equal(genericResult, fastResult);
+
+ // Verify round-trip
+ var decoded = Base58.Bitcoin.Decode(fastResult);
+ Assert.Equal(signatureData, decoded);
+
+ // Should be between 64 and 88 characters
+ Assert.InRange(fastResult.Length, 64, 88);
+ }
+}
diff --git a/src/Base58Encoding.Tests/Base58Encoding.Tests.csproj b/src/Base58Encoding.Tests/Base58Encoding.Tests.csproj
index 68ae08a..bba85c5 100644
--- a/src/Base58Encoding.Tests/Base58Encoding.Tests.csproj
+++ b/src/Base58Encoding.Tests/Base58Encoding.Tests.csproj
@@ -1,18 +1,20 @@
-
+
- net9.0
+ net10.0
+ Exe
enable
enable
false
- true
+ Base58Encoding.Tests
+ true
+ true
-
-
-
-
+
+
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
@@ -20,6 +22,15 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
all
+
+
+
+
+
+
+
+
+
diff --git a/src/Base58Encoding.Tests/Base58Tests.cs b/src/Base58Encoding.Tests/Base58Tests.cs
index 25f4d2e..4c418a0 100644
--- a/src/Base58Encoding.Tests/Base58Tests.cs
+++ b/src/Base58Encoding.Tests/Base58Tests.cs
@@ -270,28 +270,36 @@ public void Encode_WithLeadingZeroPatterns_PreservesCorrectly()
}
}
- [Fact]
- public void DecodeTable_Generation_ProducesCorrectMapping()
+ [Theory]
+ [InlineData(new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 }, "11111111111111111111111111111112")] // All zeros + 1
+ [InlineData(new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1 }, "1111111111111111111111111111115S")] // 257 case
+ [InlineData(new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }, "JEKNVnkbo3jma5nREBBJCDoXFVeKkD56V3xKrvRmWxFG")] // All 0xFF
+ [InlineData(new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE }, "JEKNVnkbo3jma5nREBBJCDoXFVeKkD56V3xKrvRmWxFF")] // All 0xFF - 1
+ public void DecodeBitcoin32Fast_WithFireDancerTestVectors_WorksCorrectly(byte[] expectedBytes, string encoded)
{
- // Test that we can generate correct decode tables using the Custom method
- const string rippleAlphabet = "rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz";
- const string flickrAlphabet = "123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ";
-
- var rippleCustom = Base58Alphabet.Custom(rippleAlphabet);
- var flickrCustom = Base58Alphabet.Custom(flickrAlphabet);
+ // Act - Test the fast decode method directly
+ var decoded = Base58.DecodeBitcoin32Fast(encoded);
- // Test simple single character encoding/decoding
- var testByte = new byte[] { 1 };
-
- var rippleEncoded = new Base58(rippleCustom).Encode(testByte);
- var rippleDecoded = new Base58(rippleCustom).Decode(rippleEncoded);
- Assert.Equal(testByte, rippleDecoded);
+ // Assert
+ Assert.NotNull(decoded);
+ Assert.Equal(expectedBytes, decoded);
+ Assert.Equal(32, decoded.Length);
+ }
- var flickrEncoded = new Base58(flickrCustom).Encode(testByte);
- var flickrDecoded = new Base58(flickrCustom).Decode(flickrEncoded);
- Assert.Equal(testByte, flickrDecoded);
+ [Theory]
+ [InlineData(new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 }, "1111111111111111111111111111111111111111111111111111111111111112")] // All zeros + 1
+ [InlineData(new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1 }, "111111111111111111111111111111111111111111111111111111111111115S")] // 257 case
+ [InlineData(new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }, "67rpwLCuS5DGA8KGZXKsVQ7dnPb9goRLoKfgGbLfQg9WoLUgNY77E2jT11fem3coV9nAkguBACzrU1iyZM4B8roQ")] // All 0xFF
+ [InlineData(new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE }, "67rpwLCuS5DGA8KGZXKsVQ7dnPb9goRLoKfgGbLfQg9WoLUgNY77E2jT11fem3coV9nAkguBACzrU1iyZM4B8roP")] // All 0xFF - 1
+ public void DecodeBitcoin64Fast_WithFireDancerTestVectors_WorksCorrectly(byte[] expectedBytes, string encoded)
+ {
+ // Act - Test the fast decode method directly
+ var decoded = Base58.DecodeBitcoin64Fast(encoded);
- // The encoded results should be different character sets
- Assert.NotEqual(rippleEncoded, flickrEncoded);
+ // Assert
+ Assert.NotNull(decoded);
+ Assert.Equal(expectedBytes, decoded);
+ Assert.Equal(64, decoded.Length);
}
-}
\ No newline at end of file
+
+}
diff --git a/src/Base58Encoding.Tests/BigEndianTests.cs b/src/Base58Encoding.Tests/BigEndianTests.cs
new file mode 100644
index 0000000..abaaf58
--- /dev/null
+++ b/src/Base58Encoding.Tests/BigEndianTests.cs
@@ -0,0 +1,62 @@
+using System.Diagnostics;
+
+namespace Base58Encoding.Tests;
+
+public class BigEndianTests
+{
+ private readonly ITestOutputHelper _output;
+
+ public BigEndianTests(ITestOutputHelper output)
+ {
+ _output = output;
+ }
+
+ ///
+ /// Alternatively can be run manually to verify compatibility on big-endian systems using below command
+ /// docker run -it --rm --platform linux/s390x -v ProjectPath/src:/src registry.access.redhat.com/dotnet/sdk:10.0 sh -c "cd /src/Base58Encoding.Tests && dotnet run"
+ ///
+ [Fact(Skip = "For local usage only")]
+ public void Base58Encoding_Works_On_BigEndian_ViaProcess()
+ {
+ if (!BitConverter.IsLittleEndian)
+ {
+ _output.WriteLine("Skipping big-endian verification as already running on big-endian environment.");
+ return;
+ }
+
+ var path = FindSrcPath();
+
+ var process = new Process
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ FileName = "docker",
+ Arguments = $"run --rm --platform linux/s390x -v {path}:/src registry.access.redhat.com/dotnet/sdk:10.0 sh -c \"cd /src/Base58Encoding.Tests && dotnet run --no-restore --no-build\"",
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false
+ }
+ };
+
+ process.Start();
+ string output = process.StandardOutput.ReadToEnd();
+ process.WaitForExit();
+
+ Assert.Equal(0, process.ExitCode);
+ Assert.Contains("Passed!", output);
+
+ _output.Write(output);
+ }
+
+ private static string FindSrcPath()
+ {
+ var dir = new DirectoryInfo(Directory.GetCurrentDirectory());
+ while (dir != null && dir.Name != "src" && !dir.GetFiles("*.slnx").Any())
+ {
+ dir = dir.Parent;
+ }
+
+ if (dir == null) throw new Exception("Src path not found.");
+ return Path.GetFullPath(dir.FullName);
+ }
+}
diff --git a/src/Base58Encoding.Tests/GlobalUsings.cs b/src/Base58Encoding.Tests/GlobalUsings.cs
deleted file mode 100644
index 8c927eb..0000000
--- a/src/Base58Encoding.Tests/GlobalUsings.cs
+++ /dev/null
@@ -1 +0,0 @@
-global using Xunit;
\ No newline at end of file
diff --git a/src/Base58Encoding.Tests/SimpleLeadingZerosTest.cs b/src/Base58Encoding.Tests/SimpleLeadingZerosTest.cs
index ff65c32..1df5cdc 100644
--- a/src/Base58Encoding.Tests/SimpleLeadingZerosTest.cs
+++ b/src/Base58Encoding.Tests/SimpleLeadingZerosTest.cs
@@ -27,4 +27,4 @@ public void BitcoinAddress_CountLeadingZerosMultipleWays_SameResult()
Assert.Equal(simdScalarCount, manualCount);
}
-}
\ No newline at end of file
+}
diff --git a/src/Base58Encoding.Tests/xunit.runner.json b/src/Base58Encoding.Tests/xunit.runner.json
new file mode 100644
index 0000000..503b748
--- /dev/null
+++ b/src/Base58Encoding.Tests/xunit.runner.json
@@ -0,0 +1,4 @@
+{
+ "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
+ "parallelAlgorithm": "aggressive"
+}
diff --git a/src/Base58Encoding/CountLeading.Base58.cs b/src/Base58Encoding/Base58.CountLeading.cs
similarity index 94%
rename from src/Base58Encoding/CountLeading.Base58.cs
rename to src/Base58Encoding/Base58.CountLeading.cs
index ade5ea8..11001bf 100644
--- a/src/Base58Encoding/CountLeading.Base58.cs
+++ b/src/Base58Encoding/Base58.CountLeading.cs
@@ -1,4 +1,4 @@
-using System.Numerics;
+using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
@@ -7,10 +7,9 @@ namespace Base58Encoding;
public partial class Base58
{
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static int CountLeadingZeros(ReadOnlySpan data)
{
- if (data.Length < 64)
+ if (data.Length < 32)
return CountLeadingZerosScalar(data);
int count = CountLeadingZerosSimd(data, out int processed);
@@ -20,7 +19,6 @@ internal static int CountLeadingZeros(ReadOnlySpan data)
return count + CountLeadingZerosScalar(data.Slice(count));
}
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static int CountLeadingZerosSimd(ReadOnlySpan data, out int processed)
{
int count = 0;
@@ -72,7 +70,6 @@ internal static int CountLeadingZerosSimd(ReadOnlySpan data, out int proce
return count;
}
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static int CountLeadingZerosScalar(ReadOnlySpan data)
{
int count = 0;
diff --git a/src/Base58Encoding/Base58.Decode.cs b/src/Base58Encoding/Base58.Decode.cs
new file mode 100644
index 0000000..b534926
--- /dev/null
+++ b/src/Base58Encoding/Base58.Decode.cs
@@ -0,0 +1,285 @@
+using System.Buffers.Binary;
+
+namespace Base58Encoding;
+
+public partial class Base58
+{
+ ///
+ /// Decode Base58 string to byte array
+ ///
+ /// Base58 encoded string
+ /// Decoded byte array
+ /// Invalid Base58 character
+ public byte[] Decode(ReadOnlySpan encoded)
+ {
+ if (encoded.IsEmpty)
+ return [];
+
+ // Hot path for Bitcoin alphabet + common expected output sizes
+ if (ReferenceEquals(this, _bitcoin.Value))
+ {
+ // Only use fast decode for lengths that STRONGLY suggest fixed sizes
+ // These are the maximum-length encodings that are very likely to be exactly 32/64 bytes
+ return encoded.Length switch
+ {
+ >= 43 and <= 44 => DecodeBitcoin32Fast(encoded) ?? DecodeGeneric(encoded), // Very likely 32 bytes
+ >= 87 and <= 88 => DecodeBitcoin64Fast(encoded) ?? DecodeGeneric(encoded), // Very likely 64 bytes
+ _ => DecodeGeneric(encoded)
+ };
+ }
+
+ // Fallback for other alphabets
+ return DecodeGeneric(encoded);
+ }
+
+ ///
+ /// Decode Base58 string to byte array using generic algorithm
+ ///
+ /// Base58 encoded string
+ /// Decoded byte array
+ /// Invalid Base58 character
+ internal byte[] DecodeGeneric(ReadOnlySpan encoded)
+ {
+ if (encoded.IsEmpty)
+ return [];
+
+ int leadingOnes = CountLeadingCharacters(encoded, _firstCharacter);
+
+ int outputSize = encoded.Length * 733 / 1000 + 1;
+
+ Span decoded = outputSize > MaxStackallocByte
+ ? new byte[outputSize]
+ : stackalloc byte[outputSize];
+
+ int decodedLength = 1;
+ decoded[0] = 0;
+
+ var decodeTable = _decodeTable.Span;
+
+ for (int i = leadingOnes; i < encoded.Length; i++)
+ {
+ char c = encoded[i];
+
+ if (c >= 128 || decodeTable[c] == 255)
+ ThrowHelper.ThrowInvalidCharacter(c);
+
+ int carry = decodeTable[c];
+
+ for (int j = 0; j < decodedLength; j++)
+ {
+ carry += decoded[j] * Base;
+ decoded[j] = (byte)(carry & 0xFF);
+ carry >>= 8;
+ }
+
+ while (carry > 0)
+ {
+ decoded[decodedLength++] = (byte)(carry & 0xFF);
+ carry >>= 8;
+ }
+ }
+
+ // If we only have leading ones and no other digits were processed,
+ // we should only return the leading zeros (not add an extra byte)
+ int actualDecodedLength = (leadingOnes == encoded.Length) ? 0 : decodedLength;
+
+ var result = new byte[leadingOnes + actualDecodedLength];
+
+ if (actualDecodedLength > 0)
+ {
+ var finalDecoded = decoded.Slice(0, decodedLength);
+ finalDecoded.Reverse();
+ finalDecoded.CopyTo(result.AsSpan(leadingOnes));
+ }
+
+ return result;
+ }
+
+ internal static byte[]? DecodeBitcoin32Fast(ReadOnlySpan encoded)
+ {
+ int charCount = encoded.Length;
+
+ // Convert to raw base58 digits with validation + conversion in one pass
+ Span rawBase58 = stackalloc byte[Base58BitcoinTables.Raw58Sz32]; // 45 bytes
+ var bitcoinDecodeTable = Base58Alphabet.Bitcoin.DecodeTable.Span;
+
+ // Prepend zeros to make exactly Raw58Sz32 characters
+ int prepend0 = Base58BitcoinTables.Raw58Sz32 - charCount;
+ for (int j = 0; j < Base58BitcoinTables.Raw58Sz32; j++)
+ {
+ if (j < prepend0)
+ {
+ rawBase58[j] = 0;
+ }
+ else
+ {
+ char c = encoded[j - prepend0];
+ // Validate + convert using Bitcoin decode table (return null for invalid chars)
+ if (c >= 128 || bitcoinDecodeTable[c] == 255)
+ return null;
+
+ rawBase58[j] = bitcoinDecodeTable[c];
+ }
+ }
+
+ // Convert to intermediate format (base 58^5)
+ Span intermediate = stackalloc ulong[Base58BitcoinTables.IntermediateSz32]; // 9 elements
+
+ for (int i = 0; i < Base58BitcoinTables.IntermediateSz32; i++)
+ {
+ intermediate[i] = (ulong)rawBase58[5 * i + 0] * 11316496UL + // 58^4
+ (ulong)rawBase58[5 * i + 1] * 195112UL + // 58^3
+ (ulong)rawBase58[5 * i + 2] * 3364UL + // 58^2
+ (ulong)rawBase58[5 * i + 3] * 58UL + // 58^1
+ (ulong)rawBase58[5 * i + 4] * 1UL; // 58^0
+ }
+
+ // Convert to overcomplete base 2^32 using decode table
+ Span binary = stackalloc ulong[Base58BitcoinTables.BinarySz32]; // 8 elements
+
+ for (int j = 0; j < Base58BitcoinTables.BinarySz32; j++)
+ {
+ ulong acc = 0UL;
+ for (int i = 0; i < Base58BitcoinTables.IntermediateSz32; i++)
+ {
+ acc += intermediate[i] * Base58BitcoinTables.DecodeTable32[i][j];
+ }
+ binary[j] = acc;
+ }
+
+ // Reduce each term to less than 2^32
+ for (int i = Base58BitcoinTables.BinarySz32 - 1; i > 0; i--)
+ {
+ binary[i - 1] += (binary[i] >> 32);
+ binary[i] &= 0xFFFFFFFFUL;
+ }
+
+ // Check if the result is too large for 32 bytes
+ if (binary[0] > 0xFFFFFFFFUL) return null;
+
+ // Convert to big-endian byte output
+ var result = new byte[32];
+ for (int i = 0; i < Base58BitcoinTables.BinarySz32; i++)
+ {
+ uint value = (uint)binary[i];
+ int offset = i * sizeof(uint);
+ BinaryPrimitives.WriteUInt32BigEndian(result.AsSpan(offset, sizeof(uint)), value);
+ }
+
+ // Count leading zeros in output
+ int outputLeadingZeros = 0;
+ for (int i = 0; i < 32; i++)
+ {
+ if (result[i] != 0) break;
+ outputLeadingZeros++;
+ }
+
+ // Count leading '1's in input
+ int inputLeadingOnes = 0;
+ for (int i = 0; i < encoded.Length; i++)
+ {
+ if (encoded[i] != '1') break;
+ inputLeadingOnes++;
+ }
+
+ // Leading zeros in output must match leading '1's in input
+ if (outputLeadingZeros != inputLeadingOnes) return null;
+
+ // Return the full 32 bytes - the result should always be 32 bytes for 32-byte decode
+ return result;
+ }
+
+ internal static byte[]? DecodeBitcoin64Fast(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
+ var bitcoinDecodeTable = Base58Alphabet.Bitcoin.DecodeTable.Span;
+
+ // Prepend zeros to make exactly Raw58Sz64 characters
+ int prepend0 = Base58BitcoinTables.Raw58Sz64 - charCount;
+ for (int j = 0; j < Base58BitcoinTables.Raw58Sz64; j++)
+ {
+ if (j < prepend0)
+ {
+ rawBase58[j] = 0;
+ }
+ else
+ {
+ char c = encoded[j - prepend0];
+ // Validate + convert using Bitcoin decode table (return null for invalid chars)
+ if (c >= 128 || bitcoinDecodeTable[c] == 255)
+ return null;
+
+ rawBase58[j] = bitcoinDecodeTable[c];
+ }
+ }
+
+ // Convert to intermediate format (base 58^5)
+ Span intermediate = stackalloc ulong[Base58BitcoinTables.IntermediateSz64]; // 18 elements
+
+ for (int i = 0; i < Base58BitcoinTables.IntermediateSz64; i++)
+ {
+ intermediate[i] = (ulong)rawBase58[5 * i + 0] * 11316496UL + // 58^4
+ (ulong)rawBase58[5 * i + 1] * 195112UL + // 58^3
+ (ulong)rawBase58[5 * i + 2] * 3364UL + // 58^2
+ (ulong)rawBase58[5 * i + 3] * 58UL + // 58^1
+ (ulong)rawBase58[5 * i + 4] * 1UL; // 58^0
+ }
+
+ // Convert to overcomplete base 2^32 using decode table
+ Span binary = stackalloc ulong[Base58BitcoinTables.BinarySz64]; // 16 elements
+
+ for (int j = 0; j < Base58BitcoinTables.BinarySz64; j++)
+ {
+ ulong acc = 0UL;
+ for (int i = 0; i < Base58BitcoinTables.IntermediateSz64; i++)
+ {
+ acc += intermediate[i] * Base58BitcoinTables.DecodeTable64[i][j];
+ }
+ binary[j] = acc;
+ }
+
+ // Reduce each term to less than 2^32
+ for (int i = Base58BitcoinTables.BinarySz64 - 1; i > 0; i--)
+ {
+ binary[i - 1] += (binary[i] >> 32);
+ binary[i] &= 0xFFFFFFFFUL;
+ }
+
+ // Check if the result is too large for 64 bytes
+ if (binary[0] > 0xFFFFFFFFUL) return null;
+
+ // Convert to big-endian byte output
+ var result = new byte[64];
+ for (int i = 0; i < Base58BitcoinTables.BinarySz64; i++)
+ {
+ uint value = (uint)binary[i];
+ int offset = i * sizeof(uint);
+ BinaryPrimitives.WriteUInt32BigEndian(result.AsSpan(offset, sizeof(uint)), value);
+ }
+
+ // Count leading zeros in output
+ int outputLeadingZeros = 0;
+ for (int i = 0; i < 64; i++)
+ {
+ if (result[i] != 0) break;
+ outputLeadingZeros++;
+ }
+
+ // Count leading '1's in input
+ int inputLeadingOnes = 0;
+ for (int i = 0; i < encoded.Length; i++)
+ {
+ if (encoded[i] != '1') break;
+ inputLeadingOnes++;
+ }
+
+ // Leading zeros in output must match leading '1's in input
+ if (outputLeadingZeros != inputLeadingOnes) return null;
+
+ // Return the full 64 bytes - the result should always be 64 bytes for 64-byte decode
+ return result;
+ }
+}
diff --git a/src/Base58Encoding/Base58.Encode.cs b/src/Base58Encoding/Base58.Encode.cs
new file mode 100644
index 0000000..12d6f20
--- /dev/null
+++ b/src/Base58Encoding/Base58.Encode.cs
@@ -0,0 +1,320 @@
+using System.Buffers.Binary;
+using System.Diagnostics;
+
+namespace Base58Encoding;
+
+public partial class Base58
+{
+ ///
+ /// Encode byte array to Base58 string
+ ///
+ /// Bytes to encode
+ /// Base58 encoded string
+ public string Encode(ReadOnlySpan data)
+ {
+ if (data.IsEmpty)
+ return string.Empty;
+
+ // Hot path for Bitcoin alphabet + common sizes
+ if (ReferenceEquals(this, _bitcoin.Value))
+ {
+ return data.Length switch
+ {
+ 32 => EncodeBitcoin32Fast(data),
+ 64 => EncodeBitcoin64Fast(data),
+ _ => EncodeGeneric(data)
+ };
+ }
+
+ // Fallback for other alphabets
+ return EncodeGeneric(data);
+ }
+
+ ///
+ /// Encode byte array to Base58 string using generic algorithm
+ ///
+ /// Bytes to encode
+ /// Base58 encoded string
+ internal string EncodeGeneric(ReadOnlySpan data)
+ {
+ if (data.IsEmpty)
+ return string.Empty;
+
+ int leadingZeros = CountLeadingZeros(data);
+
+ if (leadingZeros == data.Length)
+ {
+ return new string(_firstCharacter, leadingZeros);
+ }
+
+ var inputSpan = data[leadingZeros..];
+
+ var size = (inputSpan.Length * 137 / 100) + 1;
+ Span digits = size > MaxStackallocByte
+ ? new byte[size]
+ : stackalloc byte[size];
+
+ int digitCount = 1;
+ digits[0] = 0;
+
+ foreach (byte b in inputSpan)
+ {
+ int carry = b;
+
+ for (int i = 0; i < digitCount; i++)
+ {
+ carry += digits[i] << 8;
+ carry = Math.DivRem(carry, Base, out int remainder);
+ digits[i] = (byte)remainder;
+ }
+
+ while (carry > 0)
+ {
+ carry = Math.DivRem(carry, Base, out int remainder);
+ digits[digitCount++] = (byte)remainder;
+ }
+ }
+
+ int resultSize = leadingZeros + digitCount;
+ return string.Create(resultSize, new EncodeGenericFinalString(_characters.Span, digits, _firstCharacter, leadingZeros, digitCount), static (span, state) =>
+ {
+ if (state.LeadingZeroes > 0)
+ {
+ span[..state.LeadingZeroes].Fill(state.FirstCharacter);
+ }
+
+ int index = state.LeadingZeroes;
+ for (int i = state.DigitCount - 1; i >= 0; i--)
+ {
+ span[index++] = state.Alphabet[state.Digits[i]];
+ }
+ });
+ }
+
+ internal static string EncodeBitcoin32Fast(ReadOnlySpan data)
+ {
+ // Count leading zeros (needed for final output)
+ int inLeadingZeros = CountLeadingZeros(data);
+
+ if (inLeadingZeros == data.Length)
+ {
+ return new string('1', inLeadingZeros);
+ }
+
+ // Convert 32 bytes to 8 uint32 limbs (big-endian)
+ Span binary = stackalloc uint[Base58BitcoinTables.BinarySz32];
+ for (int i = 0; i < Base58BitcoinTables.BinarySz32; i++)
+ {
+ int offset = i * sizeof(uint);
+ binary[i] = BinaryPrimitives.ReadUInt32BigEndian(data.Slice(offset, sizeof(uint)));
+ }
+
+ // Convert to intermediate format (base 58^5)
+ Span intermediate = stackalloc ulong[Base58BitcoinTables.IntermediateSz32];
+ intermediate.Clear();
+
+ // Matrix multiplication: intermediate = binary * EncodeTable32
+ for (int i = 0; i < Base58BitcoinTables.BinarySz32; i++)
+ {
+ for (int j = 0; j < Base58BitcoinTables.IntermediateSz32 - 1; j++)
+ {
+ intermediate[j + 1] += (ulong)binary[i] * Base58BitcoinTables.EncodeTable32[i][j];
+ }
+ }
+
+ // Reduce each term to be less than 58^5
+ for (int i = Base58BitcoinTables.IntermediateSz32 - 1; i > 0; i--)
+ {
+ intermediate[i - 1] += intermediate[i] / Base58BitcoinTables.R1Div;
+ intermediate[i] %= Base58BitcoinTables.R1Div;
+ }
+
+ // Convert intermediate form to raw base58 digits
+ Span rawBase58 = stackalloc byte[Base58BitcoinTables.Raw58Sz32];
+ for (int i = 0; i < Base58BitcoinTables.IntermediateSz32; i++)
+ {
+ uint v = (uint)intermediate[i];
+
+ rawBase58[5 * i + 4] = (byte)((v / 1U) % 58U);
+ rawBase58[5 * i + 3] = (byte)((v / 58U) % 58U);
+ 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
+ int rawLeadingZeros = 0;
+ for (; rawLeadingZeros < Base58BitcoinTables.Raw58Sz32; rawLeadingZeros++)
+ {
+ if (rawBase58[rawLeadingZeros] != 0) break;
+ }
+
+ // Calculate skip and final length (match Firedancer exactly)
+ int skip = rawLeadingZeros - inLeadingZeros;
+ 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');
+ }
+
+ // Convert remaining raw base58 digits to characters
+ // Read from rawLeadingZeros onwards (where the actual digits are)
+ var bitcoinChars = Base58BitcoinTables.BitcoinChars;
+ for (int i = 0; i < state.OutputLength - state.InLeadingZeros; i++)
+ {
+ byte digit = state.RawBase58[state.RawLeadingZeros + i];
+ Debug.Assert(digit < 58, $"Base58 digit should always be < 58, got {digit}");
+ span[state.InLeadingZeros + i] = bitcoinChars[digit];
+ }
+ });
+ }
+
+ private static string EncodeBitcoin64Fast(ReadOnlySpan data)
+ {
+ // Count leading zeros (needed for final output)
+ int inLeadingZeros = CountLeadingZeros(data);
+
+ if (inLeadingZeros == data.Length)
+ {
+ return new string('1', inLeadingZeros);
+ }
+
+ // Convert 64 bytes to 16 uint32 limbs (big-endian)
+ Span binary = stackalloc uint[Base58BitcoinTables.BinarySz64];
+ for (int i = 0; i < Base58BitcoinTables.BinarySz64; i++)
+ {
+ int offset = i * sizeof(uint);
+ binary[i] = BinaryPrimitives.ReadUInt32BigEndian(data.Slice(offset, sizeof(uint)));
+ }
+
+ // Convert to intermediate format (base 58^5)
+ Span intermediate = stackalloc ulong[Base58BitcoinTables.IntermediateSz64];
+ intermediate.Clear();
+
+ // Matrix multiplication: intermediate = binary * EncodeTable64
+ // For 64-byte, we need to handle potential overflow like Firedancer does
+ for (int i = 0; i < 8; i++)
+ {
+ for (int j = 0; j < Base58BitcoinTables.IntermediateSz64 - 1; j++)
+ {
+ intermediate[j + 1] += (ulong)binary[i] * Base58BitcoinTables.EncodeTable64[i][j];
+ }
+ }
+
+ // Mini-reduction to prevent overflow (like Firedancer)
+ intermediate[15] += intermediate[16] / Base58BitcoinTables.R1Div;
+ intermediate[16] %= Base58BitcoinTables.R1Div;
+
+ // Finish remaining iterations
+ for (int i = 8; i < Base58BitcoinTables.BinarySz64; i++)
+ {
+ for (int j = 0; j < Base58BitcoinTables.IntermediateSz64 - 1; j++)
+ {
+ intermediate[j + 1] += (ulong)binary[i] * Base58BitcoinTables.EncodeTable64[i][j];
+ }
+ }
+
+ // Reduce each term to be less than 58^5
+ for (int i = Base58BitcoinTables.IntermediateSz64 - 1; i > 0; i--)
+ {
+ intermediate[i - 1] += intermediate[i] / Base58BitcoinTables.R1Div;
+ intermediate[i] %= Base58BitcoinTables.R1Div;
+ }
+
+ // Convert intermediate form to raw base58 digits
+ Span rawBase58 = stackalloc byte[Base58BitcoinTables.Raw58Sz64];
+ for (int i = 0; i < Base58BitcoinTables.IntermediateSz64; i++)
+ {
+ uint v = (uint)intermediate[i];
+ rawBase58[5 * i + 4] = (byte)((v / 1U) % 58U);
+ rawBase58[5 * i + 3] = (byte)((v / 58U) % 58U);
+ rawBase58[5 * i + 2] = (byte)((v / 3364U) % 58U);
+ 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,
+ $"Invalid base58 digit generated at position {i} - algorithm bug");
+ }
+
+ // Count leading zeros in raw output
+ int rawLeadingZeros = 0;
+ for (; rawLeadingZeros < Base58BitcoinTables.Raw58Sz64; rawLeadingZeros++)
+ {
+ 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');
+ }
+
+ // Convert remaining raw base58 digits to characters
+ // Read from rawLeadingZeros onwards (where the actual digits are)
+ var bitcoinChars = Base58BitcoinTables.BitcoinChars;
+ for (int i = 0; i < state.OutputLength - state.InLeadingZeros; i++)
+ {
+ byte digit = state.RawBase58[state.RawLeadingZeros + i];
+ Debug.Assert(digit < 58, $"Base58 digit should always be < 58, got {digit}");
+ span[state.InLeadingZeros + i] = bitcoinChars[digit];
+ }
+ });
+ }
+
+ internal readonly ref struct EncodeFastState
+ {
+ public readonly ReadOnlySpan RawBase58;
+ public readonly int InLeadingZeros;
+ public readonly int RawLeadingZeros;
+ public readonly int OutputLength;
+
+ public EncodeFastState(ReadOnlySpan rawBase58, int inLeadingZeros, int rawLeadingZeros, int outputLength)
+ {
+ RawBase58 = rawBase58;
+ InLeadingZeros = inLeadingZeros;
+ RawLeadingZeros = rawLeadingZeros;
+ OutputLength = outputLength;
+ }
+ }
+
+ internal readonly ref struct EncodeGenericFinalString
+ {
+ public readonly ReadOnlySpan Alphabet;
+ public readonly ReadOnlySpan Digits;
+ public readonly char FirstCharacter;
+ public readonly int LeadingZeroes;
+ public readonly int DigitCount;
+
+ public EncodeGenericFinalString(ReadOnlySpan alphabet, ReadOnlySpan digits, char firstCharacter, int leadingZeroes, int digitCount)
+ {
+ Alphabet = alphabet;
+ Digits = digits;
+ FirstCharacter = firstCharacter;
+ LeadingZeroes = leadingZeroes;
+ DigitCount = digitCount;
+ }
+ }
+}
diff --git a/src/Base58Encoding/Base58.cs b/src/Base58Encoding/Base58.cs
index d0fc14d..32af5f6 100644
--- a/src/Base58Encoding/Base58.cs
+++ b/src/Base58Encoding/Base58.cs
@@ -1,5 +1,3 @@
-using System.Runtime.CompilerServices;
-
namespace Base58Encoding;
public partial class Base58
@@ -19,146 +17,10 @@ public partial class Base58
public static Base58 Ripple => _ripple.Value;
public static Base58 Flickr => _flickr.Value;
- public Base58(Base58Alphabet alphabet)
+ private Base58(Base58Alphabet alphabet)
{
_characters = alphabet.Characters;
_decodeTable = alphabet.DecodeTable;
_firstCharacter = alphabet.FirstCharacter;
}
-
- ///
- /// Encode byte array to Base58 string
- ///
- /// Bytes to encode
- /// Base58 encoded string
- public string Encode(ReadOnlySpan data)
- {
- if (data.IsEmpty)
- return string.Empty;
-
- int leadingZeros = CountLeadingZeros(data);
-
- if (leadingZeros == data.Length)
- {
- return new string(_firstCharacter, leadingZeros);
- }
-
- var inputSpan = data[leadingZeros..];
-
- var size = (inputSpan.Length * 137 / 100) + 1;
- Span digits = size > MaxStackallocByte
- ? new byte[size]
- : stackalloc byte[size];
-
- int digitCount = 1;
- digits[0] = 0;
-
- foreach (byte b in inputSpan)
- {
- int carry = b;
-
- for (int i = 0; i < digitCount; i++)
- {
- carry += digits[i] << 8;
- carry = Math.DivRem(carry, Base, out int remainder);
- digits[i] = (byte)remainder;
- }
-
- while (carry > 0)
- {
- carry = Math.DivRem(carry, Base, out int remainder);
- digits[digitCount++] = (byte)remainder;
- }
- }
-
- int resultSize = leadingZeros + digitCount;
- return string.Create(resultSize, new FinalString(_characters.Span, digits, _firstCharacter, leadingZeros, digitCount), static (span, state) =>
- {
- if (state.LeadingZeroes > 0)
- {
- span[..state.LeadingZeroes].Fill(state.FirstCharacter);
- }
-
- int index = state.LeadingZeroes;
- for (int i = state.DigitCount - 1; i >= 0; i--)
- {
- span[index++] = state.Alphabet[state.Digits[i]];
- }
- });
- }
-
- ///
- /// Decode Base58 string to byte array
- ///
- /// Base58 encoded string
- /// Decoded byte array
- /// Invalid Base58 character
- public byte[] Decode(ReadOnlySpan encoded)
- {
- if (encoded.IsEmpty)
- return [];
-
- int leadingOnes = CountLeadingCharacters(encoded, _firstCharacter);
-
- int outputSize = encoded.Length * 733 / 1000 + 1;
-
- Span decoded = outputSize > MaxStackallocByte
- ? new byte[outputSize]
- : stackalloc byte[outputSize];
-
- int decodedLength = 1;
- decoded[0] = 0;
-
- var decodeTable = _decodeTable.Span;
-
- for (int i = leadingOnes; i < encoded.Length; i++)
- {
- char c = encoded[i];
-
- if (c >= 128 || decodeTable[c] == 255)
- ThrowHelper.ThrowInvalidCharacter(c);
-
- int carry = decodeTable[c];
-
- for (int j = 0; j < decodedLength; j++)
- {
- carry += decoded[j] * Base;
- decoded[j] = (byte)(carry & 0xFF);
- carry >>= 8;
- }
-
- while (carry > 0)
- {
- decoded[decodedLength++] = (byte)(carry & 0xFF);
- carry >>= 8;
- }
- }
-
- var result = new byte[leadingOnes + decodedLength];
-
- var finalDecoded = decoded.Slice(0, decodedLength);
- finalDecoded.Reverse();
-
- finalDecoded.CopyTo(result.AsSpan(leadingOnes));
-
- return result;
- }
-
- private readonly ref struct FinalString
- {
- public readonly ReadOnlySpan Alphabet;
- public readonly ReadOnlySpan Digits;
- public readonly char FirstCharacter;
- public readonly int LeadingZeroes;
- public readonly int DigitCount;
-
- public FinalString(ReadOnlySpan alphabet, ReadOnlySpan digits, char firstCharacter, int leadingZeroes, int digitCount)
- {
- Alphabet = alphabet;
- Digits = digits;
- FirstCharacter = firstCharacter;
- LeadingZeroes = leadingZeroes;
- DigitCount = digitCount;
- }
- }
-}
\ No newline at end of file
+}
diff --git a/src/Base58Encoding/Base58Alphabet.cs b/src/Base58Encoding/Base58Alphabet.cs
index b2acd68..bf908aa 100644
--- a/src/Base58Encoding/Base58Alphabet.cs
+++ b/src/Base58Encoding/Base58Alphabet.cs
@@ -42,7 +42,7 @@ private Base58Alphabet(ReadOnlyMemory characters, ReadOnlyMemory dec
// Static decode tables - using ReadOnlyMemory for better performance
private static readonly ReadOnlyMemory BitcoinDecodeTable = new byte[]
-{
+ {
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
@@ -51,7 +51,7 @@ private Base58Alphabet(ReadOnlyMemory characters, ReadOnlyMemory dec
22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 255, 255, 255, 255, 255,
255, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 255, 44, 45, 46,
47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 255, 255, 255, 255, 255
-};
+ };
private static readonly ReadOnlyMemory RippleDecodeTable = new byte[]
{
@@ -76,25 +76,4 @@ private Base58Alphabet(ReadOnlyMemory characters, ReadOnlyMemory dec
255, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 255, 20, 21, 22,
23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 255, 255, 255, 255, 255
};
-
- public static Base58Alphabet Custom(string characters)
- {
- if (characters.Length != 58)
- {
- ThrowHelper.ThrowNotExactLength();
- }
-
- var decodeTable = new byte[128];
- for (int i = 0; i < 128; i++)
- {
- decodeTable[i] = 255;
- }
-
- for (int i = 0; i < characters.Length; i++)
- {
- decodeTable[characters[i]] = (byte)i;
- }
-
- return new Base58Alphabet(characters.AsMemory(), decodeTable, characters[0]);
- }
-}
\ No newline at end of file
+}
diff --git a/src/Base58Encoding/Base58BitcoinTables.cs b/src/Base58Encoding/Base58BitcoinTables.cs
new file mode 100644
index 0000000..b3ac9e4
--- /dev/null
+++ b/src/Base58Encoding/Base58BitcoinTables.cs
@@ -0,0 +1,98 @@
+using System;
+
+namespace Base58Encoding;
+
+internal static class Base58BitcoinTables
+{
+ internal const uint R1Div = 656356768U; // 58^5
+ internal const byte InvalidChar = 255;
+ internal const byte InverseTableOffset = (byte)'1'; // Characters are offset by '1'
+
+ // Bitcoin alphabet for fast character mapping
+ internal static ReadOnlySpan BitcoinChars => "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
+
+ // Constants for 32-byte encoding/decoding (from Firedancer)
+ internal const int BinarySz32 = 8;
+ internal const int IntermediateSz32 = 9;
+ internal const int Raw58Sz32 = IntermediateSz32 * 5; // 45
+
+ // Contains the unique values less than 58^5 such that:
+ // 2^(32*(7-j)) = sum_k EncodeTable32[j][k]*58^(5*(7-k))
+ internal static readonly uint[][] EncodeTable32 =
+ [
+ [513735U, 77223048U, 437087610U, 300156666U, 605448490U, 214625350U, 141436834U, 379377856U],
+ [0U, 78508U, 646269101U, 118408823U, 91512303U, 209184527U, 413102373U, 153715680U],
+ [0U, 0U, 11997U, 486083817U, 3737691U, 294005210U, 247894721U, 289024608U],
+ [0U, 0U, 0U, 1833U, 324463681U, 385795061U, 551597588U, 21339008U],
+ [0U, 0U, 0U, 0U, 280U, 127692781U, 389432875U, 357132832U],
+ [0U, 0U, 0U, 0U, 0U, 42U, 537767569U, 410450016U],
+ [0U, 0U, 0U, 0U, 0U, 0U, 6U, 356826688U],
+ [0U, 0U, 0U, 0U, 0U, 0U, 0U, 1U]
+ ];
+
+ // Contains the unique values less than 2^32 such that:
+ // 58^(5*(8-j)) = sum_k DecodeTable32[j][k]*2^(32*(7-k))
+ internal static readonly uint[][] DecodeTable32 =
+ [
+ [1277U, 2650397687U, 3801011509U, 2074386530U, 3248244966U, 687255411U, 2959155456U, 0U],
+ [0U, 8360U, 1184754854U, 3047609191U, 3418394749U, 132556120U, 1199103528U, 0U],
+ [0U, 0U, 54706U, 2996985344U, 1834629191U, 3964963911U, 485140318U, 1073741824U],
+ [0U, 0U, 0U, 357981U, 1476998812U, 3337178590U, 1483338760U, 4194304000U],
+ [0U, 0U, 0U, 0U, 2342503U, 3052466824U, 2595180627U, 17825792U],
+ [0U, 0U, 0U, 0U, 0U, 15328518U, 1933902296U, 4063920128U],
+ [0U, 0U, 0U, 0U, 0U, 0U, 100304420U, 3355157504U],
+ [0U, 0U, 0U, 0U, 0U, 0U, 0U, 656356768U],
+ [0U, 0U, 0U, 0U, 0U, 0U, 0U, 1U]
+ ];
+
+ // Constants for 64-byte encoding/decoding (from Firedancer)
+ internal const int BinarySz64 = 16;
+ internal const int IntermediateSz64 = 18;
+ internal const int Raw58Sz64 = IntermediateSz64 * 5; // 90
+
+ // Contains the unique values less than 58^5 such that:
+ // 2^(32*(15-j)) = sum_k EncodeTable64[j][k]*58^(5*(17-k))
+ internal static readonly uint[][] EncodeTable64 =
+ [
+ [2631U, 149457141U, 577092685U, 632289089U, 81912456U, 221591423U, 502967496U, 403284731U, 377738089U, 492128779U, 746799U, 366351977U, 190199623U, 38066284U, 526403762U, 650603058U, 454901440U],
+ [0U, 402U, 68350375U, 30641941U, 266024478U, 208884256U, 571208415U, 337765723U, 215140626U, 129419325U, 480359048U, 398051646U, 635841659U, 214020719U, 136986618U, 626219915U, 49699360U],
+ [0U, 0U, 61U, 295059608U, 141201404U, 517024870U, 239296485U, 527697587U, 212906911U, 453637228U, 467589845U, 144614682U, 45134568U, 184514320U, 644355351U, 104784612U, 308625792U],
+ [0U, 0U, 0U, 9U, 256449755U, 500124311U, 479690581U, 372802935U, 413254725U, 487877412U, 520263169U, 176791855U, 78190744U, 291820402U, 74998585U, 496097732U, 59100544U],
+ [0U, 0U, 0U, 0U, 1U, 285573662U, 455976778U, 379818553U, 100001224U, 448949512U, 109507367U, 117185012U, 347328982U, 522665809U, 36908802U, 577276849U, 64504928U],
+ [0U, 0U, 0U, 0U, 0U, 0U, 143945778U, 651677945U, 281429047U, 535878743U, 264290972U, 526964023U, 199595821U, 597442702U, 499113091U, 424550935U, 458949280U],
+ [0U, 0U, 0U, 0U, 0U, 0U, 0U, 21997789U, 294590275U, 148640294U, 595017589U, 210481832U, 404203788U, 574729546U, 160126051U, 430102516U, 44963712U],
+ [0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 3361701U, 325788598U, 30977630U, 513969330U, 194569730U, 164019635U, 136596846U, 626087230U, 503769920U],
+ [0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 513735U, 77223048U, 437087610U, 300156666U, 605448490U, 214625350U, 141436834U, 379377856U],
+ [0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 78508U, 646269101U, 118408823U, 91512303U, 209184527U, 413102373U, 153715680U],
+ [0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 11997U, 486083817U, 3737691U, 294005210U, 247894721U, 289024608U],
+ [0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 1833U, 324463681U, 385795061U, 551597588U, 21339008U],
+ [0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 280U, 127692781U, 389432875U, 357132832U],
+ [0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 42U, 537767569U, 410450016U],
+ [0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 6U, 356826688U],
+ [0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 1U]
+ ];
+
+ // Contains the unique values less than 2^32 such that:
+ // 58^(5*(17-j)) = sum_k DecodeTable64[j][k]*2^(32*(15-k))
+ internal static readonly uint[][] DecodeTable64 =
+ [
+ [249448U, 3719864065U, 173911550U, 4021557284U, 3115810883U, 2498525019U, 1035889824U, 627529458U, 3840888383U, 3728167192U, 2901437456U, 3863405776U, 1540739182U, 1570766848U, 0U, 0U],
+ [0U, 1632305U, 1882780341U, 4128706713U, 1023671068U, 2618421812U, 2005415586U, 1062993857U, 3577221846U, 3960476767U, 1695615427U, 2597060712U, 669472826U, 104923136U, 0U, 0U],
+ [0U, 0U, 10681231U, 1422956801U, 2406345166U, 4058671871U, 2143913881U, 4169135587U, 2414104418U, 2549553452U, 997594232U, 713340517U, 2290070198U, 1103833088U, 0U, 0U],
+ [0U, 0U, 0U, 69894212U, 1038812943U, 1785020643U, 1285619000U, 2301468615U, 3492037905U, 314610629U, 2761740102U, 3410618104U, 1699516363U, 910779968U, 0U, 0U],
+ [0U, 0U, 0U, 0U, 457363084U, 927569770U, 3976106370U, 1389513021U, 2107865525U, 3716679421U, 1828091393U, 2088408376U, 439156799U, 2579227194U, 0U, 0U],
+ [0U, 0U, 0U, 0U, 0U, 2992822783U, 383623235U, 3862831115U, 112778334U, 339767049U, 1447250220U, 486575164U, 3495303162U, 2209946163U, 268435456U, 0U],
+ [0U, 0U, 0U, 0U, 0U, 4U, 2404108010U, 2962826229U, 3998086794U, 1893006839U, 2266258239U, 1429430446U, 307953032U, 2361423716U, 176160768U, 0U],
+ [0U, 0U, 0U, 0U, 0U, 0U, 29U, 3596590989U, 3044036677U, 1332209423U, 1014420882U, 868688145U, 4264082837U, 3688771808U, 2485387264U, 0U],
+ [0U, 0U, 0U, 0U, 0U, 0U, 0U, 195U, 1054003707U, 3711696540U, 582574436U, 3549229270U, 1088536814U, 2338440092U, 1468637184U, 0U],
+ [0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 1277U, 2650397687U, 3801011509U, 2074386530U, 3248244966U, 687255411U, 2959155456U, 0U],
+ [0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 8360U, 1184754854U, 3047609191U, 3418394749U, 132556120U, 1199103528U, 0U],
+ [0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 54706U, 2996985344U, 1834629191U, 3964963911U, 485140318U, 1073741824U],
+ [0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 357981U, 1476998812U, 3337178590U, 1483338760U, 4194304000U],
+ [0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 2342503U, 3052466824U, 2595180627U, 17825792U],
+ [0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 15328518U, 1933902296U, 4063920128U],
+ [0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 100304420U, 3355157504U],
+ [0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 656356768U],
+ [0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 1U]
+ ];
+}
diff --git a/src/Base58Encoding/Base58Encoding.csproj b/src/Base58Encoding/Base58Encoding.csproj
index 94be1cb..253c759 100644
--- a/src/Base58Encoding/Base58Encoding.csproj
+++ b/src/Base58Encoding/Base58Encoding.csproj
@@ -1,34 +1,34 @@
-
- net9.0;net10.0
- enable
- enable
+
+ net10.0
+ enable
+ enable
- Base58Encoding
- Nikolay Zdravkov
- A high-performance Base58 encoding/decoding library for .NET
- base58;encoding;bitcoin;cryptocurrency
- https://github.com/unsafePtr/Base58Encoding
- https://github.com/unsafePtr/Base58Encoding
- MIT
- README.md
- false
-
+ Base58Encoding
+ Nikolay Zdravkov
+ A high-performance Base58 encoding/decoding library for .NET
+ base58;encoding;bitcoin;cryptocurrency
+ https://github.com/unsafePtr/Base58Encoding
+ https://github.com/unsafePtr/Base58Encoding
+ MIT
+ README.md
+ false
+
-
-
- <_Parameter1>Base58Encoding.Benchmarks
-
-
-
-
- <_Parameter1>Base58Encoding.Tests
-
-
+
+
+ <_Parameter1>Base58Encoding.Benchmarks
+
+
+
+
+ <_Parameter1>Base58Encoding.Tests
+
+
-
-
-
+
+
+
diff --git a/src/Base58Encoding/ThrowHelper.cs b/src/Base58Encoding/ThrowHelper.cs
index 97cf10b..642b310 100644
--- a/src/Base58Encoding/ThrowHelper.cs
+++ b/src/Base58Encoding/ThrowHelper.cs
@@ -1,14 +1,9 @@
-using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.CodeAnalysis;
namespace Base58Encoding;
+
internal static class ThrowHelper
{
- [DoesNotReturn]
- public static void ThrowNotExactLength()
- {
- throw new InvalidOperationException("Alphabet must be exactly 58 characters long.");
- }
-
[DoesNotReturn]
public static void ThrowInvalidCharacter(char character)
{